# HG changeset patch # User Alain Mazy # Date 1589288304 -7200 # Node ID 82e88ff003d7115061f4dcd98534a32b2123cc4b # Parent dba48c162b7b978a164374b9185b873dfc2ed5fa# Parent 6f11b3233a066d4057f8dc7c95587463b4ba1d91 merge default -> c-get diff -r dba48c162b7b -r 82e88ff003d7 .hgignore --- a/.hgignore Tue May 12 07:50:38 2020 +0200 +++ b/.hgignore Tue May 12 14:58:24 2020 +0200 @@ -6,3 +6,10 @@ .vs/ .vscode/ *~ + +# when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore: +.settings/ +.classpath +.project +Resources/Testing/Issue32/Java/bin +Resources/Testing/Issue32/Java/target diff -r dba48c162b7b -r 82e88ff003d7 CMakeLists.txt --- a/CMakeLists.txt Tue May 12 07:50:38 2020 +0200 +++ b/CMakeLists.txt Tue May 12 14:58:24 2020 +0200 @@ -13,6 +13,7 @@ set(ENABLE_CRYPTO_OPTIONS ON) set(ENABLE_DCMTK ON) set(ENABLE_DCMTK_NETWORKING ON) +set(ENABLE_DCMTK_TRANSCODING ON) set(ENABLE_GOOGLE_TEST ON) set(ENABLE_JPEG ON) set(ENABLE_LOCALE ON) @@ -25,9 +26,6 @@ set(ENABLE_WEB_SERVER ON) set(ENABLE_ZLIB ON) -# To test transcoding -#set(ENABLE_DCMTK_TRANSCODING ON) - set(HAS_EMBEDDED_RESOURCES ON) diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomAssociation.cpp --- a/Core/DicomNetworking/DicomAssociation.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/DicomAssociation.cpp Tue May 12 14:58:24 2020 +0200 @@ -259,10 +259,10 @@ LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << parameters.GetLocalApplicationEntityTitle() - << "\" to AET \"" << parameters.GetRemoteApplicationEntityTitle() - << "\" on host " << parameters.GetRemoteHost() - << ":" << parameters.GetRemotePort() - << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")"; + << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle() + << "\" on host " << parameters.GetRemoteModality().GetHost() + << ":" << parameters.GetRemoteModality().GetPortNumber() + << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ")"; CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); @@ -270,7 +270,7 @@ // Set this application's title and the called application's title in the params CheckConnecting(parameters, ASC_setAPTitles( params_, parameters.GetLocalApplicationEntityTitle().c_str(), - parameters.GetRemoteApplicationEntityTitle().c_str(), NULL)); + parameters.GetRemoteModality().GetApplicationEntityTitle().c_str(), NULL)); // Set the network addresses of the local and remote entities char localHost[HOST_NAME_MAX]; @@ -284,7 +284,8 @@ snprintf #endif (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", - parameters.GetRemoteHost().c_str(), parameters.GetRemotePort()); + parameters.GetRemoteModality().GetHost().c_str(), + parameters.GetRemoteModality().GetPortNumber()); CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort)); @@ -339,7 +340,7 @@ else { LOG(WARNING) << "Unknown transfer syntax received from AET \"" - << parameters.GetRemoteApplicationEntityTitle() + << parameters.GetRemoteModality().GetApplicationEntityTitle() << "\": " << pc->acceptedTransferSyntax; } } @@ -352,7 +353,7 @@ { throw OrthancException(ErrorCode_NoPresentationContext, "Unable to negotiate a presentation context with AET \"" + - parameters.GetRemoteApplicationEntityTitle() + "\""); + parameters.GetRemoteModality().GetApplicationEntityTitle() + "\""); } } @@ -517,7 +518,7 @@ throw OrthancException(ErrorCode_NetworkProtocol, "DicomAssociation - " + command + " to AET \"" + - parameters.GetRemoteApplicationEntityTitle() + + parameters.GetRemoteModality().GetApplicationEntityTitle() + "\": " + info); } } @@ -604,7 +605,7 @@ **/ LOG(INFO) << "Reporting modality \"" - << parameters.GetRemoteApplicationEntityTitle() + << parameters.GetRemoteModality().GetApplicationEntityTitle() << "\" about storage commitment transaction: " << transactionUid << " (" << successSopClassUids.size() << " successes, " << failedSopClassUids.size() << " failures)"; @@ -654,7 +655,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to send N-EVENT-REPORT request to AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (!DIMSE_sendMessageUsingMemoryData( @@ -682,7 +683,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to read N-EVENT-REPORT response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP; @@ -696,14 +697,14 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Badly formatted N-EVENT-REPORT response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (content.DimseStatus != 0 /* success */) { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "The request cannot be handled by remote AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } } @@ -767,7 +768,7 @@ **/ LOG(INFO) << "Request to modality \"" - << parameters.GetRemoteApplicationEntityTitle() + << parameters.GetRemoteModality().GetApplicationEntityTitle() << "\" about storage commitment for " << sopClassUids.size() << " instances, with transaction UID: " << transactionUid; const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++; @@ -801,7 +802,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to send N-ACTION request to AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (!DIMSE_sendMessageUsingMemoryData( @@ -829,7 +830,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to read N-ACTION response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP; @@ -843,14 +844,14 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Badly formatted N-ACTION response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (content.DimseStatus != 0 /* success */) { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "The request cannot be handled by remote AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomAssociationParameters.cpp --- a/Core/DicomNetworking/DicomAssociationParameters.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/DicomAssociationParameters.cpp Tue May 12 14:58:24 2020 +0200 @@ -37,6 +37,7 @@ #include "../Compatibility.h" #include "../Logging.h" #include "../OrthancException.h" +#include "../SerializationToolbox.h" #include "NetworkingCompatibility.h" #include @@ -48,68 +49,104 @@ namespace Orthanc { - void DicomAssociationParameters::ReadDefaultTimeout() + void DicomAssociationParameters::CheckHost(const std::string& host) + { + if (host.size() > HOST_NAME_MAX - 10) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Invalid host name (too long): " + host); + } + } + + + uint32_t DicomAssociationParameters::GetDefaultTimeout() { boost::mutex::scoped_lock lock(defaultTimeoutMutex_); - timeout_ = defaultTimeout_; + return defaultTimeout_; } DicomAssociationParameters::DicomAssociationParameters() : - localAet_("STORESCU"), - remoteAet_("ANY-SCP"), - remoteHost_("127.0.0.1"), - remotePort_(104), - manufacturer_(ModalityManufacturer_Generic) + localAet_("ORTHANC"), + timeout_(GetDefaultTimeout()) { - ReadDefaultTimeout(); + remote_.SetApplicationEntityTitle("ANY-SCP"); } DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet, const RemoteModalityParameters& remote) : localAet_(localAet), - remoteAet_(remote.GetApplicationEntityTitle()), - remoteHost_(remote.GetHost()), - remotePort_(remote.GetPortNumber()), - manufacturer_(remote.GetManufacturer()), - timeout_(defaultTimeout_) + timeout_(GetDefaultTimeout()) { - ReadDefaultTimeout(); + SetRemoteModality(remote); } - void DicomAssociationParameters::SetRemoteHost(const std::string& host) + void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& remote) { - if (host.size() > HOST_NAME_MAX - 10) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Invalid host name (too long): " + host); - } - - remoteHost_ = host; + CheckHost(remote.GetHost()); + remote_ = remote; } - void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& parameters) + void DicomAssociationParameters::SetRemoteHost(const std::string& host) { - SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); - SetRemoteHost(parameters.GetHost()); - SetRemotePort(parameters.GetPortNumber()); - SetRemoteManufacturer(parameters.GetManufacturer()); + CheckHost(host); + remote_.SetHost(host); } bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const { return (localAet_ == other.localAet_ && - remoteAet_ == other.remoteAet_ && - remoteHost_ == other.remoteHost_ && - remotePort_ == other.remotePort_ && - manufacturer_ == other.manufacturer_); + remote_.GetApplicationEntityTitle() == other.remote_.GetApplicationEntityTitle() && + remote_.GetHost() == other.remote_.GetHost() && + remote_.GetPortNumber() == other.remote_.GetPortNumber() && + remote_.GetManufacturer() == other.remote_.GetManufacturer() && + timeout_ == other.timeout_); } + + static const char* const LOCAL_AET = "LocalAet"; + static const char* const REMOTE = "Remote"; + static const char* const TIMEOUT = "Timeout"; // New in Orthanc in 1.7.0 + + + void DicomAssociationParameters::SerializeJob(Json::Value& target) const + { + if (target.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + target[LOCAL_AET] = localAet_; + remote_.Serialize(target[REMOTE], true /* force advanced format */); + target[TIMEOUT] = timeout_; + } + } + + + DicomAssociationParameters DicomAssociationParameters::UnserializeJob(const Json::Value& serialized) + { + if (serialized.type() == Json::objectValue) + { + DicomAssociationParameters result; + result.remote_ = RemoteModalityParameters(serialized[REMOTE]); + result.localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); + result.timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, GetDefaultTimeout()); + + return result; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds) { LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomAssociationParameters.h --- a/Core/DicomNetworking/DicomAssociationParameters.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/DicomAssociationParameters.h Tue May 12 14:58:24 2020 +0200 @@ -35,6 +35,8 @@ #include "RemoteModalityParameters.h" +#include + class OFCondition; // From DCMTK namespace Orthanc @@ -42,14 +44,11 @@ class DicomAssociationParameters { private: - std::string localAet_; - std::string remoteAet_; - std::string remoteHost_; - uint16_t remotePort_; - ModalityManufacturer manufacturer_; - uint32_t timeout_; + std::string localAet_; + RemoteModalityParameters remote_; + uint32_t timeout_; - void ReadDefaultTimeout(); + static void CheckHost(const std::string& host); public: DicomAssociationParameters(); @@ -62,52 +61,38 @@ return localAet_; } - const std::string& GetRemoteApplicationEntityTitle() const - { - return remoteAet_; - } - - const std::string& GetRemoteHost() const - { - return remoteHost_; - } - - uint16_t GetRemotePort() const - { - return remotePort_; - } - - ModalityManufacturer GetRemoteManufacturer() const - { - return manufacturer_; - } - void SetLocalApplicationEntityTitle(const std::string& aet) { localAet_ = aet; } + const RemoteModalityParameters& GetRemoteModality() const + { + return remote_; + } + + void SetRemoteModality(const RemoteModalityParameters& parameters); + void SetRemoteApplicationEntityTitle(const std::string& aet) { - remoteAet_ = aet; + remote_.SetApplicationEntityTitle(aet); } void SetRemoteHost(const std::string& host); void SetRemotePort(uint16_t port) { - remotePort_ = port; + remote_.SetPortNumber(port); } void SetRemoteManufacturer(ModalityManufacturer manufacturer) { - manufacturer_ = manufacturer; + remote_.SetManufacturer(manufacturer); } - void SetRemoteModality(const RemoteModalityParameters& parameters); - bool IsEqual(const DicomAssociationParameters& other) const; + // Setting it to "0" disables the timeout (infinite wait) void SetTimeout(uint32_t seconds) { timeout_ = seconds; @@ -122,7 +107,13 @@ { return timeout_ != 0; } + + void SerializeJob(Json::Value& target) const; + + static DicomAssociationParameters UnserializeJob(const Json::Value& serialized); static void SetDefaultTimeout(uint32_t seconds); + + static uint32_t GetDefaultTimeout(); }; } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomControlUserConnection.cpp --- a/Core/DicomNetworking/DicomControlUserConnection.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/DicomControlUserConnection.cpp Tue May 12 14:58:24 2020 +0200 @@ -257,7 +257,7 @@ if (presID == 0) { throw OrthancException(ErrorCode_DicomFindUnavailable, - "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); + "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); } T_DIMSE_C_FindRQ request; @@ -309,14 +309,14 @@ throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-FIND SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf + " (unable to process - invalid query ?)"); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } @@ -331,7 +331,7 @@ association_->Open(parameters_); std::unique_ptr query( - ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); + ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer())); DcmDataset* dataset = query->GetDcmtkObject().getDataset(); const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; @@ -362,7 +362,7 @@ if (presID == 0) { throw OrthancException(ErrorCode_DicomMoveUnavailable, - "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); + "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); } T_DIMSE_C_MoveRQ request; @@ -412,14 +412,14 @@ throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-MOVE SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf + " (unable to process - resource not found ?)"); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } @@ -434,15 +434,6 @@ } - DicomControlUserConnection::DicomControlUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote) : - parameters_(localAet, remote), - association_(new DicomAssociation) - { - SetupPresentationContexts(); - } - - void DicomControlUserConnection::Close() { assert(association_.get() != NULL); @@ -479,7 +470,7 @@ { DicomMap fields; NormalizeFindQuery(fields, level, originalFields); - query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); + query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer())); } else { @@ -525,7 +516,7 @@ const char* universal; - if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE) + if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE) { universal = "*"; } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomControlUserConnection.h --- a/Core/DicomNetworking/DicomControlUserConnection.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/DicomControlUserConnection.h Tue May 12 14:58:24 2020 +0200 @@ -65,9 +65,6 @@ const DicomMap& fields); public: - DicomControlUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote); - DicomControlUserConnection(const DicomAssociationParameters& params); const DicomAssociationParameters& GetParameters() const diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomStoreUserConnection.cpp --- a/Core/DicomNetworking/DicomStoreUserConnection.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp Tue May 12 14:58:24 2020 +0200 @@ -48,8 +48,28 @@ bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid, const std::set& syntaxes) { + // Default transfer syntax for DICOM + const bool addLittleEndianImplicit = ( + proposeUncompressedSyntaxes_ && + syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end()); + + const bool addLittleEndianExplicit = ( + proposeUncompressedSyntaxes_ && + syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end()); + + const bool addBigEndianExplicit = ( + proposeUncompressedSyntaxes_ && + proposeRetiredBigEndian_ && + syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end()); + size_t requiredCount = syntaxes.size(); - if (proposeUncompressedSyntaxes_) + if (addLittleEndianImplicit) + { + requiredCount += 1; + } + + if (addLittleEndianExplicit || + addBigEndianExplicit) { requiredCount += 1; } @@ -58,40 +78,49 @@ { return false; // Not enough room } - - for (std::set::const_iterator - it = syntaxes.begin(); it != syntaxes.end(); ++it) + else { - association_->ProposePresentationContext(sopClassUid, *it); - } - - if (proposeUncompressedSyntaxes_) - { - std::set uncompressed; - - if (syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end()) + for (std::set::const_iterator + it = syntaxes.begin(); it != syntaxes.end(); ++it) { - uncompressed.insert(DicomTransferSyntax_LittleEndianImplicit); + association_->ProposePresentationContext(sopClassUid, *it); + proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *it)); } - - if (syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end()) + + if (addLittleEndianImplicit) { - uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit); - } - - if (proposeRetiredBigEndian_ && - syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end()) - { - uncompressed.insert(DicomTransferSyntax_BigEndianExplicit); + association_->ProposePresentationContext(sopClassUid, DicomTransferSyntax_LittleEndianImplicit); + proposedOriginalClasses_.insert(std::make_pair(sopClassUid, DicomTransferSyntax_LittleEndianImplicit)); } - if (!uncompressed.empty()) + if (addLittleEndianExplicit || + addBigEndianExplicit) { + std::set uncompressed; + + if (addLittleEndianExplicit) + { + uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit); + } + + if (addBigEndianExplicit) + { + uncompressed.insert(DicomTransferSyntax_BigEndianExplicit); + } + association_->ProposePresentationContext(sopClassUid, uncompressed); + + assert(!uncompressed.empty()); + if (addLittleEndianExplicit ^ addBigEndianExplicit) + { + // Only one transfer syntax was proposed for this presentation context + assert(uncompressed.size() == 1); + proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *uncompressed.begin())); + } } - } - return true; + return true; + } } @@ -116,8 +145,8 @@ return false; } - - + + DicomStoreUserConnection::DicomStoreUserConnection( const DicomAssociationParameters& params) : parameters_(params), @@ -129,16 +158,16 @@ } - void DicomStoreUserConnection::PrepareStorageClass(const std::string& sopClassUid, - DicomTransferSyntax syntax) + void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid, + DicomTransferSyntax syntax) { - StorageClasses::iterator found = storageClasses_.find(sopClassUid); + RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid); - if (found == storageClasses_.end()) + if (found == registeredClasses_.end()) { std::set ts; ts.insert(syntax); - storageClasses_[sopClassUid] = ts; + registeredClasses_[sopClassUid] = ts; } else { @@ -147,6 +176,36 @@ } + 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, @@ -154,7 +213,7 @@ { /** * Step 1: Check whether this presentation context is already - * available in the previously negociated assocation. + * available in the previously negotiated assocation. **/ if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax)) @@ -163,22 +222,37 @@ } // The association must be re-negotiated - LOG(INFO) << "Re-negociating DICOM association with " - << parameters_.GetRemoteApplicationEntityTitle(); + if (association_->IsOpen()) + { + LOG(INFO) << "Re-negotiating DICOM association with " + << parameters_.GetRemoteModality().GetApplicationEntityTitle(); + + if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) != + proposedOriginalClasses_.end()) + { + LOG(INFO) << "The remote modality has already rejected SOP class UID \"" + << sopClassUid << "\" with transfer syntax \"" + << GetTransferSyntaxUid(transferSyntax) << "\", don't renegotiate"; + return false; + } + } + association_->ClearPresentationContexts(); - PrepareStorageClass(sopClassUid, transferSyntax); + proposedOriginalClasses_.clear(); + RegisterStorageClass(sopClassUid, transferSyntax); // (*) - + /** * Step 2: Propose at least the mandatory SOP class. **/ { - StorageClasses::const_iterator mandatory = storageClasses_.find(sopClassUid); + RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid); - if (mandatory == storageClasses_.end() || + if (mandatory == registeredClasses_.end() || mandatory->second.find(transferSyntax) == mandatory->second.end()) { + // Should never fail because of (*) throw OrthancException(ErrorCode_InternalError); } @@ -194,11 +268,11 @@ /** * Step 3: Propose all the previously spotted SOP classes, as - * registered through the "PrepareStorageClass()" method. + * registered through the "RegisterStorageClass()" method. **/ - for (StorageClasses::const_iterator it = storageClasses_.begin(); - it != storageClasses_.end(); ++it) + for (RegisteredClasses::const_iterator it = registeredClasses_.begin(); + it != registeredClasses_.end(); ++it) { if (it->first != sopClassUid) { @@ -217,15 +291,16 @@ if (proposeCommonClasses_) { + // The method "ProposeStorageClass()" will automatically add + // "LittleEndianImplicit" std::set ts; - ts.insert(DicomTransferSyntax_LittleEndianImplicit); for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++) { std::string c(dcmShortSCUStorageSOPClassUIDs[i]); if (c != sopClassUid && - storageClasses_.find(c) == storageClasses_.end()) + registeredClasses_.find(c) == registeredClasses_.end()) { ProposeStorageClass(c, ts); } @@ -245,36 +320,23 @@ void DicomStoreUserConnection::Store(std::string& sopClassUid, std::string& sopInstanceUid, - DcmDataset& dataset, + DcmFileFormat& dicom, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { - OFString a, b; - if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() || - !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good()) - { - throw OrthancException(ErrorCode_NoSopClassOrInstance, - "Unable to determine the SOP class/instance for C-STORE with AET " + - parameters_.GetRemoteApplicationEntityTitle()); - } - - sopClassUid.assign(a.c_str()); - sopInstanceUid.assign(b.c_str()); + DicomTransferSyntax transferSyntax; + LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom); - DicomTransferSyntax transferSyntax; - if (!FromDcmtkBridge::LookupOrthancTransferSyntax( - transferSyntax, dataset.getOriginalXfer())) + uint8_t presID; + if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax)) { - throw OrthancException(ErrorCode_InternalError, - "Unknown transfer syntax from DCMTK"); - } - - // Figure out which accepted presentation context should be used - uint8_t presID; - if (!NegotiatePresentationContext(presID, sopClassUid.c_str(), transferSyntax)) - { - throw OrthancException(ErrorCode_InternalError, - "No valid presentation context was negotiated upfront"); + 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 @@ -286,8 +348,8 @@ request.DataSetType = DIMSE_DATASET_PRESENT; strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN); - if (!moveOriginatorAET.empty()) - { + if (hasMoveOriginator) + { strncpy(request.MoveOriginatorApplicationEntityTitle, moveOriginatorAET.c_str(), DIC_AE_LEN); request.opts = O_STORE_MOVEORIGINATORAETITLE; @@ -296,12 +358,17 @@ 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, &dataset, /*progressCallback*/ NULL, NULL, + NULL, dicom.getDataset(), /*progressCallback*/ NULL, NULL, /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), /*opt_dimse_timeout*/ GetParameters().GetTimeout(), &response, &statusDetail, NULL), @@ -326,7 +393,7 @@ sprintf(buf, "%04X", response.DimseStatus); throw OrthancException(ErrorCode_NetworkProtocol, "C-STORE SCU to AET \"" + - GetParameters().GetRemoteApplicationEntityTitle() + + GetParameters().GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } @@ -334,26 +401,132 @@ void DicomStoreUserConnection::Store(std::string& sopClassUid, std::string& sopInstanceUid, - ParsedDicomFile& parsed, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - Store(sopClassUid, sopInstanceUid, *parsed.GetDcmtkObject().getDataset(), - moveOriginatorAET, moveOriginatorID); - } - - - void DicomStoreUserConnection::Store(std::string& sopClassUid, - std::string& sopInstanceUid, const void* buffer, size_t size, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { std::unique_ptr dicom( FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size)); - Store(sopClassUid, sopInstanceUid, *dicom->getDataset(), - moveOriginatorAET, moveOriginatorID); + if (dicom.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + } + + + void DicomStoreUserConnection::LookupTranscoding(std::set& acceptedSyntaxes, + const std::string& sopClassUid, + DicomTransferSyntax sourceSyntax) + { + acceptedSyntaxes.clear(); + + // Make sure a negotiation has already occurred for this transfer + // syntax. We don't use the return code: Transcoding is possible + // even if the "sourceSyntax" is not supported. + uint8_t presID; + NegotiatePresentationContext(presID, sopClassUid, sourceSyntax); + + std::map contexts; + if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid)) + { + for (std::map::const_iterator + it = contexts.begin(); it != contexts.end(); ++it) + { + acceptedSyntaxes.insert(it->first); + } + } + } + + + 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) + { + std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size)); + if (dicom.get() == NULL || + dicom->getDataset() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DicomTransferSyntax inputSyntax; + LookupParameters(sopClassUid, sopInstanceUid, inputSyntax, *dicom); + + std::set accepted; + LookupTranscoding(accepted, sopClassUid, inputSyntax); + + if (accepted.find(inputSyntax) != accepted.end()) + { + // No need for transcoding + Store(sopClassUid, sopInstanceUid, *dicom, + hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + } + else + { + // Transcoding is needed + std::set uncompressedSyntaxes; + + if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end()) + { + uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + } + + if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end()) + { + uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); + } + + if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end()) + { + uncompressedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit); + } + + std::unique_ptr transcoded( + transcoder.TranscodeToParsed(*dicom, buffer, size, uncompressedSyntaxes, false)); + + // WARNING: Below this point, "transcoded->GetDicom()" is possibly + // a reference to "*dicom", if the DCMTK transcoder was used + + if (transcoded.get() == NULL || + transcoded->GetDicom().getDataset() == NULL) + { + throw OrthancException( + ErrorCode_NotImplemented, + "Cannot transcode from \"" + std::string(GetTransferSyntaxUid(inputSyntax)) + + "\" to an uncompressed syntax for modality: " + + GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + } + else if (transcoded->HasSopInstanceUidChanged()) + { + throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP " + "instance UID while transcoding to an uncompressed transfer syntax"); + } + else + { + DicomTransferSyntax transcodedSyntax; + + // Sanity check + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded->GetDicom()) || + accepted.find(transcodedSyntax) == accepted.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + Store(sopClassUid, sopInstanceUid, transcoded->GetDicom(), + hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + } + } + } } } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomStoreUserConnection.h --- a/Core/DicomNetworking/DicomStoreUserConnection.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/DicomStoreUserConnection.h Tue May 12 14:58:24 2020 +0200 @@ -33,15 +33,23 @@ #pragma once +#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING) +# error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file +#endif + #include "DicomAssociationParameters.h" +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 +# include "../DicomParsing/IDicomTranscoder.h" +#endif + #include #include #include #include // For uint8_t -class DcmDataset; +class DcmFileFormat; namespace Orthanc { @@ -64,16 +72,20 @@ **/ class DicomAssociation; // Forward declaration for PImpl design pattern - class ParsedDicomFile; class DicomStoreUserConnection : public boost::noncopyable { private: - typedef std::map > StorageClasses; + typedef std::map > RegisteredClasses; + + // "ProposedOriginalClasses" keeps track of the storage classes + // that were proposed with a single transfer syntax + typedef std::set< std::pair > ProposedOriginalClasses; DicomAssociationParameters parameters_; - boost::shared_ptr association_; - StorageClasses storageClasses_; + boost::shared_ptr association_; // "shared_ptr" is for PImpl + RegisteredClasses registeredClasses_; + ProposedOriginalClasses proposedOriginalClasses_; bool proposeCommonClasses_; bool proposeUncompressedSyntaxes_; bool proposeRetiredBigEndian_; @@ -81,11 +93,18 @@ // Return "false" if there is not enough room remaining in the association bool ProposeStorageClass(const std::string& sopClassUid, const std::set& syntaxes); - - // Should only be used if transcoding + bool LookupPresentationContext(uint8_t& presentationContextId, const std::string& sopClassUid, DicomTransferSyntax transferSyntax); + + bool NegotiatePresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + + void LookupTranscoding(std::set& acceptedSyntaxes, + const std::string& sopClassUid, + DicomTransferSyntax sourceSyntax); public: DicomStoreUserConnection(const DicomAssociationParameters& params); @@ -125,23 +144,13 @@ return proposeRetiredBigEndian_; } - void PrepareStorageClass(const std::string& sopClassUid, - DicomTransferSyntax syntax); - - // TODO => to private - bool NegotiatePresentationContext(uint8_t& presentationContextId, - const std::string& sopClassUid, - DicomTransferSyntax transferSyntax); + void RegisterStorageClass(const std::string& sopClassUid, + DicomTransferSyntax syntax); void Store(std::string& sopClassUid, std::string& sopInstanceUid, - DcmDataset& dataset, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void Store(std::string& sopClassUid, - std::string& sopInstanceUid, - ParsedDicomFile& parsed, + DcmFileFormat& dicom, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); @@ -149,7 +158,22 @@ std::string& sopInstanceUid, const void* buffer, size_t size, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); + + void LookupParameters(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomTransferSyntax& transferSyntax, + DcmFileFormat& dicom); + + void 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); }; } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomUserConnection.cpp --- a/Core/DicomNetworking/DicomUserConnection.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1822 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - - -/*========================================================================= - - This file is based on portions of the following project: - - Program: DCMTK 3.6.0 - Module: http://dicom.offis.de/dcmtk.php.en - -Copyright (C) 1994-2011, OFFIS e.V. -All rights reserved. - -This software and supporting documentation were developed by - - OFFIS e.V. - R&D Division Health - Escherweg 2 - 26121 Oldenburg, Germany - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -- Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -- Neither the name of OFFIS nor the names of its contributors may be - used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -=========================================================================*/ - - -#include "../PrecompiledHeaders.h" -#include "DicomUserConnection.h" - -#if !defined(DCMTK_VERSION_NUMBER) -# error The macro DCMTK_VERSION_NUMBER must be defined -#endif - -#include "../Compatibility.h" -#include "../DicomFormat/DicomArray.h" -#include "../Logging.h" -#include "../OrthancException.h" -#include "../DicomParsing/FromDcmtkBridge.h" -#include "../DicomParsing/ToDcmtkBridge.h" -#include "NetworkingCompatibility.h" - -#include -#include -#include -#include -#include -#include - -#include - - -static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax; - -/** - * "If we have more than 64 storage SOP classes, tools such as - * storescu will fail because they attempt to negotiate two - * presentation contexts for each SOP class, and there is a total - * limit of 128 contexts for one association." - **/ -static const unsigned int MAXIMUM_STORAGE_SOP_CLASSES = 64; - - -namespace Orthanc -{ - // By default, the timeout for DICOM SCU (client) connections is set to 10 seconds - static uint32_t defaultTimeout_ = 10; - - struct DicomUserConnection::PImpl - { - // Connection state - uint32_t dimseTimeout_; - uint32_t acseTimeout_; - T_ASC_Network* net_; - T_ASC_Parameters* params_; - T_ASC_Association* assoc_; - - bool IsOpen() const - { - return assoc_ != NULL; - } - - void CheckIsOpen() const; - - void Store(std::string& sopClassUidOut /* out */, - std::string& sopInstanceUidOut /* out */, - DcmInputStream& is, - DicomUserConnection& connection, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - }; - - - static void Check(const OFCondition& cond, - const std::string& aet, - const std::string& command) - { - if (cond.bad()) - { - // Reformat the error message from DCMTK by turning multiline - // errors into a single line - - std::string s(cond.text()); - std::string info; - info.reserve(s.size()); - - bool isMultiline = false; - for (size_t i = 0; i < s.size(); i++) - { - if (s[i] == '\r') - { - // Ignore - } - else if (s[i] == '\n') - { - if (isMultiline) - { - info += "; "; - } - else - { - info += " ("; - isMultiline = true; - } - } - else - { - info.push_back(s[i]); - } - } - - if (isMultiline) - { - info += ")"; - } - - throw OrthancException(ErrorCode_NetworkProtocol, - "DicomUserConnection - " + command + - " to AET \"" + aet + "\": " + info); - } - } - - void DicomUserConnection::PImpl::CheckIsOpen() const - { - if (!IsOpen()) - { - throw OrthancException(ErrorCode_NetworkProtocol, - "DicomUserConnection: First open the connection"); - } - } - - - void DicomUserConnection::CheckIsOpen() const - { - pimpl_->CheckIsOpen(); - } - - - static void RegisterStorageSOPClass(T_ASC_Parameters* params, - unsigned int& presentationContextId, - const std::string& sopClass, - const char* asPreferred[], - std::vector& asFallback, - const std::string& aet) - { - Check(ASC_addPresentationContext(params, presentationContextId, - sopClass.c_str(), asPreferred, 1), - aet, "initializing"); - presentationContextId += 2; - - if (asFallback.size() > 0) - { - Check(ASC_addPresentationContext(params, presentationContextId, - sopClass.c_str(), &asFallback[0], asFallback.size()), - aet, "initializing"); - presentationContextId += 2; - } - } - - - void DicomUserConnection::SetupPresentationContexts(Mode mode, - const std::string& preferredTransferSyntax) - { - // Flatten an array with the preferred transfer syntax - const char* asPreferred[1] = { preferredTransferSyntax.c_str() }; - - // Setup the fallback transfer syntaxes - std::set fallbackSyntaxes; - fallbackSyntaxes.insert(UID_LittleEndianExplicitTransferSyntax); - fallbackSyntaxes.insert(UID_BigEndianExplicitTransferSyntax); - fallbackSyntaxes.insert(UID_LittleEndianImplicitTransferSyntax); - fallbackSyntaxes.erase(preferredTransferSyntax); - - // Flatten an array with the fallback transfer syntaxes - std::vector asFallback; - asFallback.reserve(fallbackSyntaxes.size()); - for (std::set::const_iterator - it = fallbackSyntaxes.begin(); it != fallbackSyntaxes.end(); ++it) - { - asFallback.push_back(it->c_str()); - } - - CheckStorageSOPClassesInvariant(); - - switch (mode) - { - case Mode_Generic: - { - unsigned int presentationContextId = 1; - - for (std::list::const_iterator it = reservedStorageSOPClasses_.begin(); - it != reservedStorageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } - - for (std::set::const_iterator it = storageSOPClasses_.begin(); - it != storageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } - - for (std::set::const_iterator it = defaultStorageSOPClasses_.begin(); - it != defaultStorageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } - - break; - } - - case Mode_RequestStorageCommitment: - case Mode_ReportStorageCommitment: - { - const char* as = UID_StorageCommitmentPushModelSOPClass; - - std::vector ts; - ts.push_back(UID_LittleEndianExplicitTransferSyntax); - ts.push_back(UID_LittleEndianImplicitTransferSyntax); - - T_ASC_SC_ROLE role; - switch (mode) - { - case Mode_RequestStorageCommitment: - role = ASC_SC_ROLE_DEFAULT; - break; - - case Mode_ReportStorageCommitment: - role = ASC_SC_ROLE_SCP; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - Check(ASC_addPresentationContext(pimpl_->params_, 1 /*presentationContextId*/, - as, &ts[0], ts.size(), role), - remoteAet_, "initializing"); - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - static bool IsGenericTransferSyntax(const std::string& syntax) - { - return (syntax == UID_LittleEndianExplicitTransferSyntax || - syntax == UID_BigEndianExplicitTransferSyntax || - syntax == UID_LittleEndianImplicitTransferSyntax); - } - - - void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut, - std::string& sopInstanceUidOut, - DcmInputStream& is, - DicomUserConnection& connection, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - DcmFileFormat dcmff; - Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength), - connection.remoteAet_, "C-STORE"); - - // Determine the storage SOP class UID for this instance - OFString sopClassUid; - if (dcmff.getDataset()->findAndGetOFString(DCM_SOPClassUID, sopClassUid).good()) - { - connection.AddStorageSOPClass(sopClassUid.c_str()); - } - - // Determine whether a new presentation context must be - // negotiated, depending on the transfer syntax of this instance - DcmXfer xfer(dcmff.getDataset()->getOriginalXfer()); - const std::string syntax(xfer.getXferID()); - bool isGeneric = IsGenericTransferSyntax(syntax); - - bool renegotiate; - - if (!IsOpen()) - { - renegotiate = true; - } - else if (isGeneric) - { - // Are we making a generic-to-specific or specific-to-generic change of - // the transfer syntax? If this is the case, renegotiate the connection. - renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax()); - - if (renegotiate) - { - LOG(INFO) << "Use of non-generic transfer syntax: the C-Store associated must be renegotiated"; - } - } - else - { - // We are using a specific transfer syntax. Renegotiate if the - // current connection does not match this transfer syntax. - renegotiate = (syntax != connection.GetPreferredTransferSyntax()); - - if (renegotiate) - { - LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated"; - } - } - - if (renegotiate) - { - if (isGeneric) - { - connection.ResetPreferredTransferSyntax(); - } - else - { - connection.SetPreferredTransferSyntax(syntax); - } - } - - if (!connection.IsOpen()) - { - connection.Open(); - } - - // Figure out which SOP class and SOP instance is encapsulated in the file - DIC_UI sopClass; - DIC_UI sopInstance; - -#if DCMTK_VERSION_NUMBER >= 364 - if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sizeof(sopClass), sopInstance, sizeof(sopInstance))) -#else - if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance)) -#endif - { - throw OrthancException(ErrorCode_NoSopClassOrInstance, - "Unable to determine the SOP class/instance for C-STORE with AET " + - connection.remoteAet_); - } - - sopClassUidOut.assign(sopClass); - sopInstanceUidOut.assign(sopInstance); - - // Figure out which of the accepted presentation contexts should be used - int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass); - if (presID == 0) - { - const char *modalityName = dcmSOPClassUIDToModality(sopClass); - if (modalityName == NULL) modalityName = dcmFindNameOfUID(sopClass); - if (modalityName == NULL) modalityName = "unknown SOP class"; - throw OrthancException(ErrorCode_NoPresentationContext, - "Unable to determine the accepted presentation contexts for C-STORE with AET " + - connection.remoteAet_ + " (" + std::string(modalityName) + ")"); - } - - // Prepare the transmission of data - T_DIMSE_C_StoreRQ request; - memset(&request, 0, sizeof(request)); - request.MessageID = assoc_->nextMsgID++; - strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); - request.Priority = DIMSE_PRIORITY_MEDIUM; - request.DataSetType = DIMSE_DATASET_PRESENT; - strncpy(request.AffectedSOPInstanceUID, sopInstance, DIC_UI_LEN); - - if (!moveOriginatorAET.empty()) - { - 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; - } - - // Finally conduct transmission of data - T_DIMSE_C_StoreRSP response; - DcmDataset* statusDetail = NULL; - Check(DIMSE_storeUser(assoc_, presID, &request, - NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL, - /*opt_blockMode*/ (dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ dimseTimeout_, - &response, &statusDetail, NULL), - connection.remoteAet_, "C-STORE"); - - if (statusDetail != NULL) - { - delete statusDetail; - } - - - /** - * 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 - { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - throw OrthancException(ErrorCode_NetworkProtocol, - "C-STORE SCU to AET \"" + connection.remoteAet_ + - "\" has failed with DIMSE status 0x" + buf); - } - } - - - namespace - { - struct FindPayload - { - DicomFindAnswers* answers; - const char* level; - bool isWorklist; - }; - } - - - static void FindCallback( - /* in */ - void *callbackData, - T_DIMSE_C_FindRQ *request, /* original find request */ - int responseCount, - T_DIMSE_C_FindRSP *response, /* pending response received */ - DcmDataset *responseIdentifiers /* pending response identifiers */ - ) - { - FindPayload& payload = *reinterpret_cast(callbackData); - - if (responseIdentifiers != NULL) - { - if (payload.isWorklist) - { - ParsedDicomFile answer(*responseIdentifiers); - payload.answers->Add(answer); - } - else - { - DicomMap m; - FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers); - - if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) - { - m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false); - } - - payload.answers->Add(m); - } - } - } - - - static void NormalizeFindQuery(DicomMap& fixedQuery, - ResourceType level, - const DicomMap& fields) - { - std::set allowedTags; - - // WARNING: Do not add "break" or reorder items in this switch-case! - switch (level) - { - case ResourceType_Instance: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance); - - case ResourceType_Series: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Series); - - case ResourceType_Study: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Study); - - case ResourceType_Patient: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (level) - { - case ResourceType_Patient: - allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); - allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); - allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); - break; - - case ResourceType_Study: - allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); - allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); - allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); - allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY); - break; - - case ResourceType_Series: - allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); - break; - - default: - break; - } - - allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); - - DicomArray query(fields); - for (size_t i = 0; i < query.GetSize(); i++) - { - const DicomTag& tag = query.GetElement(i).GetTag(); - if (allowedTags.find(tag) == allowedTags.end()) - { - LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag; - } - else - { - fixedQuery.SetValue(tag, query.GetElement(i).GetValue()); - } - } - } - - - static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields, - ModalityManufacturer manufacturer) - { - // Fix outgoing C-Find requests issue for Syngo.Via and its - // solution was reported by Emsy Chan by private mail on - // 2015-06-17. According to Robert van Ommen (2015-11-30), the - // same fix is required for Agfa Impax. This was generalized for - // generic manufacturer since it seems to affect PhilipsADW, - // GEWAServer as well: - // https://bitbucket.org/sjodogne/orthanc/issues/31/ - - switch (manufacturer) - { - case ModalityManufacturer_GenericNoWildcardInDates: - case ModalityManufacturer_GenericNoUniversalWildcard: - { - std::unique_ptr fix(fields.Clone()); - - std::set tags; - fix->GetTags(tags); - - for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) - { - // Replace a "*" wildcard query by an empty query ("") for - // "date" or "all" value representations depending on the - // type of manufacturer. - if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard || - (manufacturer == ModalityManufacturer_GenericNoWildcardInDates && - FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date)) - { - const DicomValue* value = fix->TestAndGetValue(*it); - - if (value != NULL && - !value->IsNull() && - value->GetContent() == "*") - { - fix->SetValue(*it, "", false); - } - } - } - - return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */); - } - - default: - return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */); - } - } - - - static void ExecuteFind(DicomFindAnswers& answers, - T_ASC_Association* association, - DcmDataset* dataset, - const char* sopClass, - bool isWorklist, - const char* level, - uint32_t dimseTimeout, - const std::string& remoteAet) - { - assert(isWorklist ^ (level != NULL)); - - FindPayload payload; - payload.answers = &answers; - payload.level = level; - payload.isWorklist = isWorklist; - - // Figure out which of the accepted presentation contexts should be used - int presID = ASC_findAcceptedPresentationContextID(association, sopClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_DicomFindUnavailable, - "Remote AET is " + remoteAet); - } - - T_DIMSE_C_FindRQ request; - memset(&request, 0, sizeof(request)); - request.MessageID = association->nextMsgID++; - strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); - request.Priority = DIMSE_PRIORITY_MEDIUM; - request.DataSetType = DIMSE_DATASET_PRESENT; - - T_DIMSE_C_FindRSP response; - DcmDataset* statusDetail = NULL; - -#if DCMTK_VERSION_NUMBER >= 364 - int responseCount; -#endif - - OFCondition cond = DIMSE_findUser(association, presID, &request, dataset, -#if DCMTK_VERSION_NUMBER >= 364 - responseCount, -#endif - FindCallback, &payload, - /*opt_blockMode*/ (dimseTimeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ dimseTimeout, - &response, &statusDetail); - - if (statusDetail) - { - delete statusDetail; - } - - Check(cond, remoteAet, "C-FIND"); - - - /** - * New in Orthanc 1.6.0: Deal with failures during C-FIND. - * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1 - **/ - - if (response.DimseStatus != 0x0000 && // Success - response.DimseStatus != 0xFF00 && // Pending - Matches are continuing - response.DimseStatus != 0xFF01) // Pending - Matches are continuing - { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - - if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess) - { - throw OrthancException(ErrorCode_NetworkProtocol, - HttpStatus_422_UnprocessableEntity, - "C-FIND SCU to AET \"" + remoteAet + - "\" has failed with DIMSE status 0x" + buf + - " (unable to process - invalid query ?)" - ); - } - else - { - throw OrthancException(ErrorCode_NetworkProtocol, - "C-FIND SCU to AET \"" + remoteAet + - "\" has failed with DIMSE status 0x" + buf); - } - } - - } - - - void DicomUserConnection::Find(DicomFindAnswers& result, - ResourceType level, - const DicomMap& originalFields, - bool normalize) - { - CheckIsOpen(); - - std::unique_ptr query; - - if (normalize) - { - DicomMap fields; - NormalizeFindQuery(fields, level, originalFields); - query.reset(ConvertQueryFields(fields, manufacturer_)); - } - else - { - query.reset(new ParsedDicomFile(originalFields, - GetDefaultDicomEncoding(), - false /* be strict */)); - } - - DcmDataset* dataset = query->GetDcmtkObject().getDataset(); - - const char* clevel = NULL; - const char* sopClass = NULL; - - switch (level) - { - case ResourceType_Patient: - clevel = "PATIENT"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); - sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; - break; - - case ResourceType_Study: - clevel = "STUDY"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); - sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - break; - - case ResourceType_Series: - clevel = "SERIES"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); - sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - break; - - case ResourceType_Instance: - clevel = "IMAGE"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); - sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - - const char* universal; - if (manufacturer_ == ModalityManufacturer_GE) - { - universal = "*"; - } - else - { - universal = ""; - } - - - // Add the expected tags for this query level. - // WARNING: Do not reorder or add "break" in this switch-case! - switch (level) - { - case ResourceType_Instance: - if (!dataset->tagExists(DCM_SOPInstanceUID)) - { - DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal); - } - - case ResourceType_Series: - if (!dataset->tagExists(DCM_SeriesInstanceUID)) - { - DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal); - } - - case ResourceType_Study: - if (!dataset->tagExists(DCM_AccessionNumber)) - { - DU_putStringDOElement(dataset, DCM_AccessionNumber, universal); - } - - if (!dataset->tagExists(DCM_StudyInstanceUID)) - { - DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal); - } - - case ResourceType_Patient: - if (!dataset->tagExists(DCM_PatientID)) - { - DU_putStringDOElement(dataset, DCM_PatientID, universal); - } - - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - assert(clevel != NULL && sopClass != NULL); - ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, - pimpl_->dimseTimeout_, remoteAet_); - } - - - void DicomUserConnection::MoveInternal(const std::string& targetAet, - ResourceType level, - const DicomMap& fields) - { - CheckIsOpen(); - - std::unique_ptr query(ConvertQueryFields(fields, manufacturer_)); - DcmDataset* dataset = query->GetDcmtkObject().getDataset(); - - const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; - switch (level) - { - case ResourceType_Patient: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); - break; - - case ResourceType_Study: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); - break; - - case ResourceType_Series: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); - break; - - case ResourceType_Instance: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - // Figure out which of the accepted presentation contexts should be used - int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_DicomMoveUnavailable, - "Remote AET is " + remoteAet_); - } - - T_DIMSE_C_MoveRQ request; - memset(&request, 0, sizeof(request)); - request.MessageID = pimpl_->assoc_->nextMsgID++; - strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); - request.Priority = DIMSE_PRIORITY_MEDIUM; - request.DataSetType = DIMSE_DATASET_PRESENT; - strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN); - - T_DIMSE_C_MoveRSP response; - DcmDataset* statusDetail = NULL; - DcmDataset* responseIdentifiers = NULL; - OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset, - NULL, NULL, - /*opt_blockMode*/ (pimpl_->dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, - pimpl_->net_, NULL, NULL, - &response, &statusDetail, &responseIdentifiers); - - if (statusDetail) - { - delete statusDetail; - } - - if (responseIdentifiers) - { - delete responseIdentifiers; - } - - Check(cond, remoteAet_, "C-MOVE"); - - - /** - * New in Orthanc 1.6.0: Deal with failures during C-MOVE. - * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2 - **/ - - if (response.DimseStatus != 0x0000 && // Success - response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing - { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - - if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess) - { - throw OrthancException(ErrorCode_NetworkProtocol, - HttpStatus_422_UnprocessableEntity, - "C-MOVE SCU to AET \"" + remoteAet_ + - "\" has failed with DIMSE status 0x" + buf + - " (unable to process - resource not found ?)" - ); - } - else - { - throw OrthancException(ErrorCode_NetworkProtocol, - "C-MOVE SCU to AET \"" + remoteAet_ + - "\" has failed with DIMSE status 0x" + buf); - } - } - } - - - void DicomUserConnection::ResetStorageSOPClasses() - { - CheckStorageSOPClassesInvariant(); - - storageSOPClasses_.clear(); - defaultStorageSOPClasses_.clear(); - - // Copy the short list of storage SOP classes from DCMTK, making - // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**). - - std::set uncommon; - uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage); - uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage); - uncommon.insert(UID_ColorSoftcopyPresentationStateStorage); - uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage); - uncommon.insert(UID_XAXRFGrayscaleSoftcopyPresentationStateStorage); - - // Add the storage syntaxes for C-STORE - for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++) - { - if (uncommon.find(dcmShortSCUStorageSOPClassUIDs[i]) == uncommon.end()) - { - defaultStorageSOPClasses_.insert(dcmShortSCUStorageSOPClassUIDs[i]); - } - } - - CheckStorageSOPClassesInvariant(); - } - - - void DicomUserConnection::DefaultSetup() - { - preferredTransferSyntax_ = DEFAULT_PREFERRED_TRANSFER_SYNTAX; - localAet_ = "STORESCU"; - remoteAet_ = "ANY-SCP"; - remoteHost_ = "127.0.0.1"; - remotePort_ = 104; - manufacturer_ = ModalityManufacturer_Generic; - - SetTimeout(defaultTimeout_); - pimpl_->net_ = NULL; - pimpl_->params_ = NULL; - pimpl_->assoc_ = NULL; - - // SOP classes for C-ECHO, C-FIND and C-MOVE (**) - reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass); - reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); - reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); - reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); - reservedStorageSOPClasses_.push_back(UID_FINDModalityWorklistInformationModel); - - ResetStorageSOPClasses(); - } - - - DicomUserConnection::DicomUserConnection() : - pimpl_(new PImpl) - { - DefaultSetup(); - } - - - DicomUserConnection::DicomUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote) : - pimpl_(new PImpl) - { - DefaultSetup(); - SetLocalApplicationEntityTitle(localAet); - SetRemoteModality(remote); - } - - - DicomUserConnection::~DicomUserConnection() - { - Close(); - } - - - void DicomUserConnection::SetRemoteModality(const RemoteModalityParameters& parameters) - { - SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); - SetRemoteHost(parameters.GetHost()); - SetRemotePort(parameters.GetPortNumber()); - SetRemoteManufacturer(parameters.GetManufacturer()); - } - - - void DicomUserConnection::SetLocalApplicationEntityTitle(const std::string& aet) - { - if (localAet_ != aet) - { - Close(); - localAet_ = aet; - } - } - - void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet) - { - if (remoteAet_ != aet) - { - Close(); - remoteAet_ = aet; - } - } - - void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer) - { - if (manufacturer_ != manufacturer) - { - Close(); - manufacturer_ = manufacturer; - } - } - - void DicomUserConnection::ResetPreferredTransferSyntax() - { - SetPreferredTransferSyntax(DEFAULT_PREFERRED_TRANSFER_SYNTAX); - } - - void DicomUserConnection::SetPreferredTransferSyntax(const std::string& preferredTransferSyntax) - { - if (preferredTransferSyntax_ != preferredTransferSyntax) - { - Close(); - preferredTransferSyntax_ = preferredTransferSyntax; - } - } - - - void DicomUserConnection::SetRemoteHost(const std::string& host) - { - if (remoteHost_ != host) - { - if (host.size() > HOST_NAME_MAX - 10) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Invalid host name (too long): " + host); - } - - Close(); - remoteHost_ = host; - } - } - - void DicomUserConnection::SetRemotePort(uint16_t port) - { - if (remotePort_ != port) - { - Close(); - remotePort_ = port; - } - } - - void DicomUserConnection::OpenInternal(Mode mode) - { - if (IsOpen()) - { - // Don't reopen the connection - return; - } - - LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() - << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host " - << GetRemoteHost() << ":" << GetRemotePort() - << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")"; - - Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_), remoteAet_, "connecting"); - Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU), remoteAet_, "connecting"); - - // Set this application's title and the called application's title in the params - Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL), - remoteAet_, "connecting"); - - // Set the network addresses of the local and remote entities - char localHost[HOST_NAME_MAX]; - gethostname(localHost, HOST_NAME_MAX - 1); - - char remoteHostAndPort[HOST_NAME_MAX]; - -#ifdef _MSC_VER - _snprintf -#else - snprintf -#endif - (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_); - - Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort), - remoteAet_, "connecting"); - - // Set various options - Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false), - remoteAet_, "connecting"); - - SetupPresentationContexts(mode, preferredTransferSyntax_); - - // Do the association - Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_), - remoteAet_, "connecting"); - - if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0) - { - throw OrthancException(ErrorCode_NoPresentationContext, - "Unable to negotiate a presentation context with AET " + - remoteAet_); - } - } - - void DicomUserConnection::Close() - { - if (pimpl_->assoc_ != NULL) - { - ASC_releaseAssociation(pimpl_->assoc_); - ASC_destroyAssociation(&pimpl_->assoc_); - pimpl_->assoc_ = NULL; - pimpl_->params_ = NULL; - } - else - { - if (pimpl_->params_ != NULL) - { - ASC_destroyAssociationParameters(&pimpl_->params_); - pimpl_->params_ = NULL; - } - } - - if (pimpl_->net_ != NULL) - { - ASC_dropNetwork(&pimpl_->net_); - pimpl_->net_ = NULL; - } - } - - bool DicomUserConnection::IsOpen() const - { - return pimpl_->IsOpen(); - } - - void DicomUserConnection::Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const void* buffer, - size_t size, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - // Prepare an input stream for the memory buffer - DcmInputBufferStream is; - if (size > 0) - is.setBuffer(buffer, size); - is.setEos(); - - pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); - } - - void DicomUserConnection::Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& buffer, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - if (buffer.size() > 0) - Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(), - moveOriginatorAET, moveOriginatorID); - else - Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID); - } - - void DicomUserConnection::StoreFile(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& path, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - // Prepare an input stream for the file - DcmInputFileStream is(path.c_str()); - pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); - } - - bool DicomUserConnection::Echo() - { - CheckIsOpen(); - DIC_US status; - Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, - /*opt_blockMode*/ (pimpl_->dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, - &status, NULL), remoteAet_, "C-ECHO"); - return status == STATUS_Success; - } - - - static void TestAndCopyTag(DicomMap& result, - const DicomMap& source, - const DicomTag& tag) - { - if (!source.HasTag(tag)) - { - throw OrthancException(ErrorCode_BadRequest); - } - else - { - result.SetValue(tag, source.GetValue(tag)); - } - } - - - void DicomUserConnection::Move(const std::string& targetAet, - ResourceType level, - const DicomMap& findResult) - { - DicomMap move; - switch (level) - { - case ResourceType_Patient: - TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); - break; - - case ResourceType_Study: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - break; - - case ResourceType_Series: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); - break; - - case ResourceType_Instance: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - MoveInternal(targetAet, level, move); - } - - - void DicomUserConnection::Move(const std::string& targetAet, - const DicomMap& findResult) - { - if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) - { - throw OrthancException(ErrorCode_InternalError); - } - - const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); - ResourceType level = StringToResourceType(tmp.c_str()); - - Move(targetAet, level, findResult); - } - - - void DicomUserConnection::MovePatient(const std::string& targetAet, - const std::string& patientId) - { - DicomMap query; - query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false); - MoveInternal(targetAet, ResourceType_Patient, query); - } - - void DicomUserConnection::MoveStudy(const std::string& targetAet, - const std::string& studyUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - MoveInternal(targetAet, ResourceType_Study, query); - } - - void DicomUserConnection::MoveSeries(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); - MoveInternal(targetAet, ResourceType_Series, query); - } - - void DicomUserConnection::MoveInstance(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid, - const std::string& instanceUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); - query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false); - MoveInternal(targetAet, ResourceType_Instance, query); - } - - - void DicomUserConnection::SetTimeout(uint32_t seconds) - { - if (seconds == 0) - { - DisableTimeout(); - } - else - { - dcmConnectionTimeout.set(seconds); - pimpl_->dimseTimeout_ = seconds; - pimpl_->acseTimeout_ = seconds; // Timeout used during association negociation and ASC_releaseAssociation() - } - } - - - void DicomUserConnection::DisableTimeout() - { - /** - * Global timeout (seconds) for connecting to remote hosts. - * Default value is -1 which selects infinite timeout, i.e. blocking connect(). - */ - dcmConnectionTimeout.set(-1); - pimpl_->dimseTimeout_ = 0; - pimpl_->acseTimeout_ = 10; // Timeout used during association negociation and ASC_releaseAssociation() - } - - - void DicomUserConnection::CheckStorageSOPClassesInvariant() const - { - assert(storageSOPClasses_.size() + - defaultStorageSOPClasses_.size() + - reservedStorageSOPClasses_.size() <= MAXIMUM_STORAGE_SOP_CLASSES); - } - - void DicomUserConnection::AddStorageSOPClass(const char* sop) - { - CheckStorageSOPClassesInvariant(); - - if (storageSOPClasses_.find(sop) != storageSOPClasses_.end()) - { - // This storage SOP class is already explicitly registered. Do - // nothing. - return; - } - - if (defaultStorageSOPClasses_.find(sop) != defaultStorageSOPClasses_.end()) - { - // This storage SOP class is not explicitly registered, but is - // used by default. Just register it explicitly. - defaultStorageSOPClasses_.erase(sop); - storageSOPClasses_.insert(sop); - - CheckStorageSOPClassesInvariant(); - return; - } - - // This storage SOP class is neither explicitly, nor implicitly - // registered. Close the connection and register it explicitly. - - Close(); - - if (reservedStorageSOPClasses_.size() + - storageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) // (*) - { - // The maximum number of SOP classes is reached - ResetStorageSOPClasses(); - defaultStorageSOPClasses_.erase(sop); - } - else if (reservedStorageSOPClasses_.size() + storageSOPClasses_.size() + - defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) - { - // Make room in the default storage syntaxes - assert(!defaultStorageSOPClasses_.empty()); // Necessarily true because condition (*) is false - defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin()); - } - - // Explicitly register the new storage syntax - storageSOPClasses_.insert(sop); - - CheckStorageSOPClassesInvariant(); - } - - - void DicomUserConnection::FindWorklist(DicomFindAnswers& result, - ParsedDicomFile& query) - { - CheckIsOpen(); - - DcmDataset* dataset = query.GetDcmtkObject().getDataset(); - const char* sopClass = UID_FINDModalityWorklistInformationModel; - - ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, - NULL, pimpl_->dimseTimeout_, remoteAet_); - } - - - void DicomUserConnection::SetDefaultTimeout(uint32_t seconds) - { - LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " - << seconds << " seconds (0 = no timeout)"; - defaultTimeout_ = seconds; - } - - - bool DicomUserConnection::IsSameAssociation(const std::string& localAet, - const RemoteModalityParameters& remote) const - { - return (localAet_ == localAet && - remoteAet_ == remote.GetApplicationEntityTitle() && - remoteHost_ == remote.GetHost() && - remotePort_ == remote.GetPortNumber() && - manufacturer_ == remote.GetManufacturer()); - } - - - static void FillSopSequence(DcmDataset& dataset, - const DcmTagKey& tag, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids, - const std::vector& failureReasons, - bool hasFailureReasons) - { - assert(sopClassUids.size() == sopInstanceUids.size() && - (hasFailureReasons ? - failureReasons.size() == sopClassUids.size() : - failureReasons.empty())); - - if (sopInstanceUids.empty()) - { - // Add an empty sequence - if (!dataset.insertEmptyElement(tag).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - } - else - { - for (size_t i = 0; i < sopClassUids.size(); i++) - { - std::unique_ptr item(new DcmItem); - if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() || - !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() || - (hasFailureReasons && - !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) || - !dataset.insertSequenceItem(tag, item.release()).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - } - } - } - - - - - void DicomUserConnection::ReportStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids, - const std::vector& failureReasons) - { - if (sopClassUids.size() != sopInstanceUids.size() || - sopClassUids.size() != failureReasons.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - if (IsOpen()) - { - Close(); - } - - std::vector successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids; - std::vector failedReasons; - - successSopClassUids.reserve(sopClassUids.size()); - successSopInstanceUids.reserve(sopClassUids.size()); - failedSopClassUids.reserve(sopClassUids.size()); - failedSopInstanceUids.reserve(sopClassUids.size()); - failedReasons.reserve(sopClassUids.size()); - - for (size_t i = 0; i < sopClassUids.size(); i++) - { - switch (failureReasons[i]) - { - case StorageCommitmentFailureReason_Success: - successSopClassUids.push_back(sopClassUids[i]); - successSopInstanceUids.push_back(sopInstanceUids[i]); - break; - - case StorageCommitmentFailureReason_ProcessingFailure: - case StorageCommitmentFailureReason_NoSuchObjectInstance: - case StorageCommitmentFailureReason_ResourceLimitation: - case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported: - case StorageCommitmentFailureReason_ClassInstanceConflict: - case StorageCommitmentFailureReason_DuplicateTransactionUID: - failedSopClassUids.push_back(sopClassUids[i]); - failedSopInstanceUids.push_back(sopInstanceUids[i]); - failedReasons.push_back(failureReasons[i]); - break; - - default: - { - char buf[16]; - sprintf(buf, "%04xH", failureReasons[i]); - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Unsupported failure reason for storage commitment: " + std::string(buf)); - } - } - } - - try - { - OpenInternal(Mode_ReportStorageCommitment); - - /** - * N-EVENT-REPORT - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1 - * - * Status code: - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 - **/ - - /** - * Send the "EVENT_REPORT_RQ" request - **/ - - LOG(INFO) << "Reporting modality \"" << remoteAet_ - << "\" about storage commitment transaction: " << transactionUid - << " (" << successSopClassUids.size() << " successes, " - << failedSopClassUids.size() << " failures)"; - const DIC_US messageId = pimpl_->assoc_->nextMsgID++; - - { - T_DIMSE_Message message; - memset(&message, 0, sizeof(message)); - message.CommandField = DIMSE_N_EVENT_REPORT_RQ; - - T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ; - content.MessageID = messageId; - strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); - strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); - content.DataSetType = DIMSE_DATASET_PRESENT; - - DcmDataset dataset; - if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - - { - std::vector empty; - FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids, - successSopInstanceUids, empty, false); - } - - // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html - if (failedSopClassUids.empty()) - { - content.EventTypeID = 1; // "Storage Commitment Request Successful" - } - else - { - content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist" - - // Failure reason - // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 - FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids, - failedSopInstanceUids, failedReasons, true); - } - - int presID = ASC_findAcceptedPresentationContextID( - pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to send N-EVENT-REPORT request to AET: " + remoteAet_); - } - - if (!DIMSE_sendMessageUsingMemoryData( - pimpl_->assoc_, presID, &message, NULL /* status detail */, - &dataset, NULL /* callback */, NULL /* callback context */, - NULL /* commandSet */).good()) - { - throw OrthancException(ErrorCode_NetworkProtocol); - } - } - - /** - * Read the "EVENT_REPORT_RSP" response - **/ - - { - T_ASC_PresentationContextID presID = 0; - T_DIMSE_Message message; - - const int timeout = pimpl_->dimseTimeout_; - if (!DIMSE_receiveCommand(pimpl_->assoc_, - (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, - &presID, &message, NULL /* no statusDetail */).good() || - message.CommandField != DIMSE_N_EVENT_REPORT_RSP) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to read N-EVENT-REPORT response from AET: " + remoteAet_); - } - - const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP; - if (content.MessageIDBeingRespondedTo != messageId || - !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) || - !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) || - //(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc - std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || - std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || - content.DataSetType != DIMSE_DATASET_NULL) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Badly formatted N-EVENT-REPORT response from AET: " + remoteAet_); - } - - if (content.DimseStatus != 0 /* success */) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "The request cannot be handled by remote AET: " + remoteAet_); - } - } - - Close(); - } - catch (OrthancException&) - { - Close(); - throw; - } - } - - - - void DicomUserConnection::RequestStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids) - { - if (sopClassUids.size() != sopInstanceUids.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - for (size_t i = 0; i < sopClassUids.size(); i++) - { - if (sopClassUids[i].empty() || - sopInstanceUids[i].empty()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "The SOP class/instance UIDs cannot be empty, found: \"" + - sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\""); - } - } - - if (transactionUid.size() < 5 || - transactionUid.substr(0, 5) != "2.25.") - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - if (IsOpen()) - { - Close(); - } - - try - { - OpenInternal(Mode_RequestStorageCommitment); - - /** - * N-ACTION - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4 - * - * Status code: - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 - **/ - - /** - * Send the "N_ACTION_RQ" request - **/ - - LOG(INFO) << "Request to modality \"" << remoteAet_ - << "\" about storage commitment for " << sopClassUids.size() - << " instances, with transaction UID: " << transactionUid; - const DIC_US messageId = pimpl_->assoc_->nextMsgID++; - - { - T_DIMSE_Message message; - memset(&message, 0, sizeof(message)); - message.CommandField = DIMSE_N_ACTION_RQ; - - T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ; - content.MessageID = messageId; - strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); - strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); - content.ActionTypeID = 1; // "Request Storage Commitment" - content.DataSetType = DIMSE_DATASET_PRESENT; - - DcmDataset dataset; - if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - - { - std::vector empty; - FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false); - } - - int presID = ASC_findAcceptedPresentationContextID( - pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to send N-ACTION request to AET: " + remoteAet_); - } - - if (!DIMSE_sendMessageUsingMemoryData( - pimpl_->assoc_, presID, &message, NULL /* status detail */, - &dataset, NULL /* callback */, NULL /* callback context */, - NULL /* commandSet */).good()) - { - throw OrthancException(ErrorCode_NetworkProtocol); - } - } - - /** - * Read the "N_ACTION_RSP" response - **/ - - { - T_ASC_PresentationContextID presID = 0; - T_DIMSE_Message message; - - const int timeout = pimpl_->dimseTimeout_; - if (!DIMSE_receiveCommand(pimpl_->assoc_, - (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, - &presID, &message, NULL /* no statusDetail */).good() || - message.CommandField != DIMSE_N_ACTION_RSP) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to read N-ACTION response from AET: " + remoteAet_); - } - - const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP; - if (content.MessageIDBeingRespondedTo != messageId || - !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) || - !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) || - //(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc - std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || - std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || - content.DataSetType != DIMSE_DATASET_NULL) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Badly formatted N-ACTION response from AET: " + remoteAet_); - } - - if (content.DimseStatus != 0 /* success */) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "The request cannot be handled by remote AET: " + remoteAet_); - } - } - - Close(); - } - catch (OrthancException&) - { - Close(); - throw; - } - } -} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/DicomUserConnection.h --- a/Core/DicomNetworking/DicomUserConnection.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,253 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1 -# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1 -#endif - -#include "DicomFindAnswers.h" -#include "../Enumerations.h" -#include "RemoteModalityParameters.h" - -#include -#include -#include -#include - -namespace Orthanc -{ - class DicomUserConnection : public boost::noncopyable - { - private: - struct PImpl; - boost::shared_ptr pimpl_; - - enum Mode - { - Mode_Generic, - Mode_ReportStorageCommitment, - Mode_RequestStorageCommitment - }; - - // Connection parameters - std::string preferredTransferSyntax_; - std::string localAet_; - std::string remoteAet_; - std::string remoteHost_; - uint16_t remotePort_; - ModalityManufacturer manufacturer_; - std::set storageSOPClasses_; - std::list reservedStorageSOPClasses_; - std::set defaultStorageSOPClasses_; - - void CheckIsOpen() const; - - void SetupPresentationContexts(Mode mode, - const std::string& preferredTransferSyntax); - - void MoveInternal(const std::string& targetAet, - ResourceType level, - const DicomMap& fields); - - void ResetStorageSOPClasses(); - - void CheckStorageSOPClassesInvariant() const; - - void DefaultSetup(); - - void OpenInternal(Mode mode); - - public: - DicomUserConnection(); - - ~DicomUserConnection(); - - // This constructor corresponds to behavior of the old class - // "ReusableDicomUserConnection", without the call to "Open()" - DicomUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote); - - void SetRemoteModality(const RemoteModalityParameters& parameters); - - void SetLocalApplicationEntityTitle(const std::string& aet); - - const std::string& GetLocalApplicationEntityTitle() const - { - return localAet_; - } - - void SetRemoteApplicationEntityTitle(const std::string& aet); - - const std::string& GetRemoteApplicationEntityTitle() const - { - return remoteAet_; - } - - void SetRemoteHost(const std::string& host); - - const std::string& GetRemoteHost() const - { - return remoteHost_; - } - - void SetRemotePort(uint16_t port); - - uint16_t GetRemotePort() const - { - return remotePort_; - } - - void SetRemoteManufacturer(ModalityManufacturer manufacturer); - - ModalityManufacturer GetRemoteManufacturer() const - { - return manufacturer_; - } - - void ResetPreferredTransferSyntax(); - - void SetPreferredTransferSyntax(const std::string& preferredTransferSyntax); - - const std::string& GetPreferredTransferSyntax() const - { - return preferredTransferSyntax_; - } - - void AddStorageSOPClass(const char* sop); - - void Open() - { - OpenInternal(Mode_Generic); - } - - void Close(); - - bool IsOpen() const; - - bool Echo(); - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const void* buffer, - size_t size, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const void* buffer, - size_t size) - { - Store(sopClassUid, sopInstanceUid, buffer, size, "", 0); // Not a C-Move - } - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& buffer, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& buffer) - { - Store(sopClassUid, sopInstanceUid, buffer, "", 0); // Not a C-Move - } - - void StoreFile(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& path, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void StoreFile(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& path) - { - StoreFile(sopClassUid, sopInstanceUid, path, "", 0); // Not a C-Move - } - - void Find(DicomFindAnswers& result, - ResourceType level, - const DicomMap& fields, - bool normalize); // Whether to normalize the DICOM query - - void Move(const std::string& targetAet, - ResourceType level, - const DicomMap& findResult); - - void Move(const std::string& targetAet, - const DicomMap& findResult); - - void MovePatient(const std::string& targetAet, - const std::string& patientId); - - void MoveStudy(const std::string& targetAet, - const std::string& studyUid); - - void MoveSeries(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid); - - void MoveInstance(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid, - const std::string& instanceUid); - - void SetTimeout(uint32_t seconds); - - void DisableTimeout(); - - void FindWorklist(DicomFindAnswers& result, - ParsedDicomFile& query); - - static void SetDefaultTimeout(uint32_t seconds); - - bool IsSameAssociation(const std::string& localAet, - const RemoteModalityParameters& remote) const; - - void ReportStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids, - const std::vector& failureReasons); - - // transactionUid: To be generated by Toolbox::GenerateDicomPrivateUniqueIdentifier() - void RequestStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Tue May 12 14:58:24 2020 +0200 @@ -522,6 +522,8 @@ assert(static_cast(count) == numberOfDcmAllStorageSOPClassUIDs); #endif + // now that C-GET SCP is always enabled, the first branch of this if is useless + // TO BE ANALIZED by SJ if (!server.HasGetRequestHandlerFactory()) // dcmqrsrv.cc line 828 { cond = ASC_acceptContextsWithPreferredTransferSyntaxes( diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/RemoteModalityParameters.cpp --- a/Core/DicomNetworking/RemoteModalityParameters.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Tue May 12 14:58:24 2020 +0200 @@ -51,6 +51,7 @@ static const char* KEY_ALLOW_N_ACTION = "AllowNAction"; static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport"; static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment"; +static const char* KEY_ALLOW_TRANSCODING = "AllowTranscoding"; static const char* KEY_HOST = "Host"; static const char* KEY_MANUFACTURER = "Manufacturer"; static const char* KEY_PORT = "Port"; @@ -71,6 +72,7 @@ allowGet_ = true; allowNAction_ = true; // For storage commitment allowNEventReport_ = true; // For storage commitment + allowTranscoding_ = true; } @@ -233,6 +235,11 @@ allowNAction_ = allow; allowNEventReport_ = allow; } + + if (serialized.isMember(KEY_ALLOW_TRANSCODING)) + { + allowTranscoding_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_TRANSCODING); + } } @@ -314,7 +321,8 @@ !allowGet_ || !allowMove_ || !allowNAction_ || - !allowNEventReport_); + !allowNEventReport_ || + !allowTranscoding_); } @@ -336,6 +344,7 @@ target[KEY_ALLOW_MOVE] = allowMove_; target[KEY_ALLOW_N_ACTION] = allowNAction_; target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_; + target[KEY_ALLOW_TRANSCODING] = allowTranscoding_; } else { diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/RemoteModalityParameters.h --- a/Core/DicomNetworking/RemoteModalityParameters.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Tue May 12 14:58:24 2020 +0200 @@ -55,6 +55,7 @@ bool allowGet_; bool allowNAction_; bool allowNEventReport_; + bool allowTranscoding_; void Clear(); @@ -131,5 +132,15 @@ void Serialize(Json::Value& target, bool forceAdvancedFormat) const; + + bool IsTranscodingAllowed() const + { + return allowTranscoding_; + } + + void SetTranscodingAllowed(bool allowed) + { + allowTranscoding_ = allowed; + } }; } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/TimeoutDicomConnectionManager.cpp --- a/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Tue May 12 14:58:24 2020 +0200 @@ -62,7 +62,7 @@ } - DicomUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection() + DicomStoreUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection() { if (that_.connection_.get() == NULL) { @@ -87,10 +87,12 @@ void TimeoutDicomConnectionManager::OpenInternal(const std::string& localAet, const RemoteModalityParameters& remote) { + DicomAssociationParameters other(localAet, remote); + if (connection_.get() == NULL || - !connection_->IsSameAssociation(localAet, remote)) + !connection_->GetParameters().IsEqual(other)) { - connection_.reset(new DicomUserConnection(localAet, remote)); + connection_.reset(new DicomStoreUserConnection(other)); } } @@ -101,7 +103,7 @@ if (connection_.get() != NULL) { LOG(INFO) << "Closing inactive DICOM association with modality: " - << connection_->GetRemoteApplicationEntityTitle(); + << connection_->GetParameters().GetRemoteModality().GetApplicationEntityTitle(); connection_.reset(NULL); } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomNetworking/TimeoutDicomConnectionManager.h --- a/Core/DicomNetworking/TimeoutDicomConnectionManager.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h Tue May 12 14:58:24 2020 +0200 @@ -43,7 +43,7 @@ #include "../Compatibility.h" -#include "DicomUserConnection.h" +#include "DicomStoreUserConnection.h" #include #include @@ -56,10 +56,10 @@ class TimeoutDicomConnectionManager : public boost::noncopyable { private: - boost::mutex mutex_; - std::unique_ptr connection_; - boost::posix_time::ptime lastUse_; - boost::posix_time::time_duration timeout_; + boost::mutex mutex_; + std::unique_ptr connection_; + boost::posix_time::ptime lastUse_; + boost::posix_time::time_duration timeout_; // Mutex must be locked void TouchInternal(); @@ -85,7 +85,7 @@ ~Lock(); - DicomUserConnection& GetConnection(); + DicomStoreUserConnection& GetConnection(); }; TimeoutDicomConnectionManager() : diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/DcmtkTranscoder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DcmtkTranscoder.cpp Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,402 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DcmtkTranscoder.h" + + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG) +# error Macro ORTHANC_ENABLE_DCMTK_JPEG must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS) +# error Macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined +#endif + + +#include "FromDcmtkBridge.h" +#include "../OrthancException.h" + +#include +#include // for DJ_RPLossy +#include // for DJ_RPLossless +#include // for DJLSRepresentationParameter + + +namespace Orthanc +{ + static uint16_t GetBitsStored(DcmDataset& dataset) + { + uint16_t bitsStored; + if (dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good()) + { + return bitsStored; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing \"Bits Stored\" tag in DICOM instance"); + } + } + + + static std::string GetSopInstanceUid(DcmDataset& dataset) + { + const char* v = NULL; + + if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() && + v != NULL) + { + return std::string(v); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID"); + } + } + + + static void CheckSopInstanceUid(DcmFileFormat& dicom, + const std::string& sopInstanceUid, + bool mustEqual) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + bool ok; + + if (mustEqual) + { + ok = (GetSopInstanceUid(*dicom.getDataset()) == sopInstanceUid); + } + else + { + ok = (GetSopInstanceUid(*dicom.getDataset()) != sopInstanceUid); + } + + if (!ok) + { + throw OrthancException(ErrorCode_InternalError, + mustEqual ? "The SOP instance UID has changed unexpectedly during transcoding" : + "The SOP instance UID has not changed as expected during transcoding"); + } + } + + + void DcmtkTranscoder::SetLossyQuality(unsigned int quality) + { + if (quality <= 0 || + quality > 100) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + lossyQuality_ = quality; + } + } + + + bool DcmtkTranscoder::InplaceTranscode(bool& hasSopInstanceUidChanged /* out */, + DcmFileFormat& dicom, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + hasSopInstanceUidChanged = false; + + DicomTransferSyntax syntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, dicom)) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax"); + } + + const uint16_t bitsStored = GetBitsStored(*dicom.getDataset()); + std::string sourceSopInstanceUid = GetSopInstanceUid(*dicom.getDataset()); + + if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end()) + { + // No transcoding is needed + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + return true; + } + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() && + allowNewSopInstanceUid && + bitsStored == 8) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossy parameters(lossyQuality_); + + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, ¶meters)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, false); + hasSopInstanceUidChanged = true; + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() && + allowNewSopInstanceUid && + bitsStored <= 12) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossy parameters(lossyQuality_); + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, ¶meters)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, false); + hasSopInstanceUidChanged = true; + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14) != allowedSyntaxes.end()) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossless parameters(6 /* opt_selection_value */, + 0 /* opt_point_transform */); + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, ¶meters)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14SV1) != allowedSyntaxes.end()) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossless parameters(6 /* opt_selection_value */, + 0 /* opt_point_transform */); + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14SV1, ¶meters)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossless) != allowedSyntaxes.end()) + { + // Check out "dcmjpls/apps/dcmcjpls.cc" + DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */, + OFTrue /* opt_useLosslessProcess */); + + /** + * WARNING: This call results in a segmentation fault if using + * the DCMTK package 3.6.2 from Ubuntu 18.04. + **/ + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossless, ¶meters)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, true); + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + if (allowNewSopInstanceUid && + allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossy) != allowedSyntaxes.end()) + { + // Check out "dcmjpls/apps/dcmcjpls.cc" + DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */, + OFFalse /* opt_useLosslessProcess */); + + /** + * WARNING: This call results in a segmentation fault if using + * the DCMTK package 3.6.2 from Ubuntu 18.04. + **/ + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, ¶meters)) + { + CheckSopInstanceUid(dicom, sourceSopInstanceUid, false); + hasSopInstanceUidChanged = true; + return true; + } + } +#endif + + return false; + } + + + bool DcmtkTranscoder::IsSupported(DicomTransferSyntax syntax) + { + if (syntax == DicomTransferSyntax_LittleEndianImplicit || + syntax == DicomTransferSyntax_LittleEndianExplicit || + syntax == DicomTransferSyntax_BigEndianExplicit || + syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit) + { + return true; + } + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess1 || + syntax == DicomTransferSyntax_JPEGProcess2_4 || + syntax == DicomTransferSyntax_JPEGProcess14 || + syntax == DicomTransferSyntax_JPEGProcess14SV1) + { + return true; + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + if (syntax == DicomTransferSyntax_JPEGLSLossless || + syntax == DicomTransferSyntax_JPEGLSLossy) + { + return true; + } +#endif + + return false; + } + + + + bool DcmtkTranscoder::TranscodeParsedToBuffer( + std::string& target /* out */, + DicomTransferSyntax& sourceSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + DcmFileFormat& dicom /* in, possibly modified */, + DicomTransferSyntax targetSyntax, + bool allowNewSopInstanceUid) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, dicom)) + { + LOG(ERROR) << "Unsupport transfer syntax for transcoding"; + return false; + } + + std::set tmp; + tmp.insert(targetSyntax); + + if (InplaceTranscode(hasSopInstanceUidChanged, dicom, tmp, allowNewSopInstanceUid)) + { + DicomTransferSyntax targetSyntax2; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, dicom) && + targetSyntax == targetSyntax2 && + dicom.getDataset() != NULL) + { + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom.getDataset()); + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + return false; + } + } + + + IDicomTranscoder::TranscodedDicom* DcmtkTranscoder::TranscodeToParsed( + DcmFileFormat& dicom /* in, possibly modified */, + const void* buffer /* in, same DICOM file as "dicom" */, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, dicom)) + { + LOG(ERROR) << "Unsupport transfer syntax for transcoding"; + return NULL; + } + + bool hasSopInstanceUidChanged; + + if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end()) + { + // No transcoding is needed + return TranscodedDicom::CreateFromExternal(dicom, false /* no change in UID */); + } + else if (InplaceTranscode(hasSopInstanceUidChanged, dicom, + allowedSyntaxes, allowNewSopInstanceUid)) + { + return TranscodedDicom::CreateFromExternal(dicom, hasSopInstanceUidChanged); + } + else + { + // Cannot transcode + return NULL; + } + } +} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/DcmtkTranscoder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DcmtkTranscoder.h Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,87 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING) +# error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file +#endif + +#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1 +# error Transcoding is disabled, cannot compile this file +#endif + +#include "IDicomTranscoder.h" + +namespace Orthanc +{ + class DcmtkTranscoder : public IDicomTranscoder + { + private: + unsigned int lossyQuality_; + + public: + DcmtkTranscoder() : + lossyQuality_(90) + { + } + + void SetLossyQuality(unsigned int quality); + + unsigned int GetLossyQuality() const + { + return lossyQuality_; + } + + bool InplaceTranscode(bool& hasSopInstanceUidChanged /* out */, + DcmFileFormat& dicom, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid); + + static bool IsSupported(DicomTransferSyntax syntax); + + virtual bool TranscodeParsedToBuffer(std::string& target /* out */, + DicomTransferSyntax& sourceSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + DcmFileFormat& dicom /* in, possibly modified */, + DicomTransferSyntax targetSyntax, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + + virtual TranscodedDicom* TranscodeToParsed( + DcmFileFormat& dicom /* in, possibly modified */, + const void* buffer /* in, same DICOM file as "dicom" */, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + }; +} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Tue May 12 14:58:24 2020 +0200 @@ -121,6 +121,13 @@ #endif +#include +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 +# include +# include // include to support color images +#endif + + namespace Orthanc { static bool IsBinaryTag(const DcmTag& key) @@ -1198,51 +1205,30 @@ } } - bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, - DcmDataset& dataSet) + + + static bool SaveToMemoryBufferInternal(std::string& buffer, + DcmFileFormat& dicom, + E_TransferSyntax xfer) { - // Determine the transfer syntax which shall be used to write the - // information to the file. We always switch to the Little Endian - // syntax, with explicit length. - - // http://support.dcmtk.org/docs/dcxfer_8h-source.html - - - /** - * Note that up to Orthanc 0.7.1 (inclusive), the - * "EXS_LittleEndianExplicit" was always used to save the DICOM - * dataset into memory. We now keep the original transfer syntax - * (if available). - **/ - E_TransferSyntax xfer = dataSet.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - // No information about the original transfer syntax: This is - // most probably a DICOM dataset that was read from memory. - xfer = EXS_LittleEndianExplicit; - } - E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; - // Create the meta-header information - DcmFileFormat ff(&dataSet); - ff.validateMetaInfo(xfer); - ff.removeInvalidGroups(); - // Create a memory buffer with the proper size { - const uint32_t estimatedSize = ff.calcElementLength(xfer, encodingType); // (*) + const uint32_t estimatedSize = dicom.calcElementLength(xfer, encodingType); // (*) buffer.resize(estimatedSize); } DcmOutputBufferStream ob(&buffer[0], buffer.size()); // Fill the memory buffer with the meta-header and the dataset - ff.transferInit(); - OFCondition c = ff.write(ob, xfer, encodingType, NULL, - /*opt_groupLength*/ EGL_recalcGL, - /*opt_paddingType*/ EPD_withoutPadding); - ff.transferEnd(); + dicom.transferInit(); + OFCondition c = dicom.write(ob, xfer, encodingType, NULL, + /*opt_groupLength*/ EGL_recalcGL, + /*opt_paddingType*/ EPD_noChange, + /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0, + EWM_updateMeta /* creates new SOP instance UID on lossy */); + dicom.transferEnd(); if (c.good()) { @@ -1265,6 +1251,81 @@ return false; } } + + + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmDataset& dataSet) + { + // Determine the transfer syntax which shall be used to write the + // information to the file. If not possible, switch to the Little + // Endian syntax, with explicit length. + + // http://support.dcmtk.org/docs/dcxfer_8h-source.html + + + /** + * Note that up to Orthanc 0.7.1 (inclusive), the + * "EXS_LittleEndianExplicit" was always used to save the DICOM + * dataset into memory. We now keep the original transfer syntax + * (if available). + **/ + E_TransferSyntax xfer = dataSet.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + // No information about the original transfer syntax: This is + // most probably a DICOM dataset that was read from memory. + xfer = EXS_LittleEndianExplicit; + } + + // Create the meta-header information + DcmFileFormat ff(&dataSet); + ff.validateMetaInfo(xfer); + ff.removeInvalidGroups(); + + return SaveToMemoryBufferInternal(buffer, ff, xfer); + } + + + bool FromDcmtkBridge::Transcode(DcmFileFormat& dicom, + DicomTransferSyntax syntax, + const DcmRepresentationParameter* representation) + { + E_TransferSyntax xfer; + if (!LookupDcmtkTransferSyntax(xfer, syntax)) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + DicomTransferSyntax sourceSyntax; + bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom); + + if (!dicom.getDataset()->chooseRepresentation(xfer, representation).good() || + !dicom.getDataset()->canWriteXfer(xfer) || + !dicom.validateMetaInfo(xfer, EWM_updateMeta).good()) + { + return false; + } + else + { + dicom.removeInvalidGroups(); + + if (known) + { + LOG(INFO) << "Transcoded an image from transfer syntax " + << GetTransferSyntaxUid(sourceSyntax) << " to " + << GetTransferSyntaxUid(syntax); + } + else + { + LOG(INFO) << "Transcoded an image from unknown transfer syntax to " + << GetTransferSyntaxUid(syntax); + } + + return true; + } + } + } ValueRepresentation FromDcmtkBridge::LookupValueRepresentation(const DicomTag& tag) @@ -1659,7 +1720,7 @@ DcmTag key(tag.GetGroup(), tag.GetElement()); if (key.getEVR() != EVR_SQ) { - throw OrthancException(ErrorCode_BadParameterType); + throw OrthancException(ErrorCode_BadParameterType, "Bad Parameter type for tag " + tag.Format()); } DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key); @@ -1703,7 +1764,7 @@ } default: - throw OrthancException(ErrorCode_BadParameterType); + throw OrthancException(ErrorCode_BadParameterType, "Bad Parameter type for tag " + tag.Format()); } return element.release(); @@ -1721,7 +1782,7 @@ DcmPixelData& pixelData = dynamic_cast(*element); DcmPixelSequence* pixelSequence = NULL; if (!pixelData.getEncapsulatedRepresentation - (dataset.getOriginalXfer(), NULL, pixelSequence).good()) + (dataset.getCurrentXfer(), NULL, pixelSequence).good()) { return NULL; } @@ -1974,25 +2035,6 @@ } - bool FromDcmtkBridge::LookupTransferSyntax(std::string& result, - DcmFileFormat& dicom) - { - const char* value = NULL; - - if (dicom.getMetaInfo() != NULL && - dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() && - value != NULL) - { - result.assign(value); - return true; - } - else - { - return false; - } - } - - #if ORTHANC_ENABLE_LUA == 1 void FromDcmtkBridge::ExecuteToDicom(DicomMap& target, LuaFunctionCall& call) @@ -2073,6 +2115,12 @@ DJEncoderRegistration::registerCodecs(); # endif #endif + + LOG(INFO) << "Registering RLE codecs in DCMTK"; + DcmRLEDecoderRegistration::registerCodecs(); +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + DcmRLEEncoderRegistration::registerCodecs(); +#endif } @@ -2093,6 +2141,11 @@ DJEncoderRegistration::cleanup(); # endif #endif + + DcmRLEDecoderRegistration::cleanup(); +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + DcmRLEEncoderRegistration::cleanup(); +#endif } @@ -2578,6 +2631,33 @@ Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding); ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions); } + + + + bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target, + DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DcmDataset& dataset = *dicom.getDataset(); + + E_TransferSyntax xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + dataset.updateOriginalXfer(); + xfer = dataset.getOriginalXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax of the DICOM instance"); + } + } + + return FromDcmtkBridge::LookupOrthancTransferSyntax(target, xfer); + } } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/FromDcmtkBridge.h --- a/Core/DicomParsing/FromDcmtkBridge.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.h Tue May 12 14:58:24 2020 +0200 @@ -205,6 +205,10 @@ static bool SaveToMemoryBuffer(std::string& buffer, DcmDataset& dataSet); + static bool Transcode(DcmFileFormat& dicom, + DicomTransferSyntax syntax, + const DcmRepresentationParameter* representation); + static ValueRepresentation Convert(DcmEVR vr); static ValueRepresentation LookupValueRepresentation(const DicomTag& tag); @@ -240,9 +244,6 @@ static void FromJson(DicomMap& values, const Json::Value& result); - static bool LookupTransferSyntax(std::string& result, - DcmFileFormat& dicom); - #if ORTHANC_ENABLE_LUA == 1 static void ExecuteToDicom(DicomMap& target, LuaFunctionCall& call); @@ -276,5 +277,8 @@ static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, E_TransferSyntax source); + + static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, + DcmFileFormat& dicom); }; } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/IDicomTranscoder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/IDicomTranscoder.cpp Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,111 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "IDicomTranscoder.h" + +#include "../OrthancException.h" + +#include + +namespace Orthanc +{ + IDicomTranscoder::TranscodedDicom::TranscodedDicom(bool hasSopInstanceUidChanged) : + external_(NULL), + hasSopInstanceUidChanged_(hasSopInstanceUidChanged) + { + } + + + IDicomTranscoder::TranscodedDicom* + IDicomTranscoder::TranscodedDicom::CreateFromExternal(DcmFileFormat& dicom, + bool hasSopInstanceUidChanged) + { + std::unique_ptr transcoded(new TranscodedDicom(hasSopInstanceUidChanged)); + transcoded->external_ = &dicom; + return transcoded.release(); + } + + + IDicomTranscoder::TranscodedDicom* + IDicomTranscoder::TranscodedDicom::CreateFromInternal(DcmFileFormat* dicom, + bool hasSopInstanceUidChanged) + { + if (dicom == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + std::unique_ptr transcoded(new TranscodedDicom(hasSopInstanceUidChanged)); + transcoded->internal_.reset(dicom); + return transcoded.release(); + } + } + + + DcmFileFormat& IDicomTranscoder::TranscodedDicom::GetDicom() const + { + if (internal_.get() != NULL) + { + return *internal_.get(); + } + else if (external_ != NULL) + { + return *external_; + } + else + { + // Probably results from a call to "ReleaseDicom()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + DcmFileFormat* IDicomTranscoder::TranscodedDicom::ReleaseDicom() + { + if (internal_.get() != NULL) + { + return internal_.release(); + } + else if (external_ != NULL) + { + return new DcmFileFormat(*external_); // Clone + } + else + { + // Probably results from a call to "ReleaseDicom()" + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } +} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/IDicomTranscoder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/IDicomTranscoder.h Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,106 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Compatibility.h" +#include "../Enumerations.h" + +#include +#include + +class DcmFileFormat; + +namespace Orthanc +{ + /** + * WARNING: This class might be called from several threads at + * once. Make sure to implement proper locking. + **/ + + class IDicomTranscoder : public boost::noncopyable + { + public: + virtual ~IDicomTranscoder() + { + } + + virtual bool TranscodeParsedToBuffer(std::string& target /* out */, + DicomTransferSyntax& sourceSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + DcmFileFormat& dicom /* in, possibly modified */, + DicomTransferSyntax targetSyntax, + bool allowNewSopInstanceUid) = 0; + + + class TranscodedDicom : public boost::noncopyable + { + private: + std::unique_ptr internal_; + DcmFileFormat* external_; + bool hasSopInstanceUidChanged_; + + TranscodedDicom(bool hasSopInstanceUidChanged); + + public: + static TranscodedDicom* CreateFromExternal(DcmFileFormat& dicom, + bool hasSopInstanceUidChanged); + + static TranscodedDicom* CreateFromInternal(DcmFileFormat* dicom, + bool hasSopInstanceUidChanged); + + // TODO - Is this information used somewhere? + bool HasSopInstanceUidChanged() const + { + return hasSopInstanceUidChanged_; + } + + DcmFileFormat& GetDicom() const; + + DcmFileFormat* ReleaseDicom(); + }; + + /** + * Transcoding flavor that creates a new parsed DICOM file. A + * "std::set<>" is used to give the possible plugin the + * possibility to do a single parsing for all the possible + * transfer syntaxes. This flavor is used by C-STORE. + **/ + virtual TranscodedDicom* TranscodeToParsed( + DcmFileFormat& dicom /* in, possibly modified */, + const void* buffer /* in, same DICOM file as "dicom" */, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) = 0; + }; +} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/Internals/DicomImageDecoder.cpp --- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp Tue May 12 14:58:24 2020 +0200 @@ -676,7 +676,7 @@ ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset, unsigned int frame) { - E_TransferSyntax syntax = dataset.getOriginalXfer(); + E_TransferSyntax syntax = dataset.getCurrentXfer(); /** * Deal with uncompressed, raw images. diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/MemoryBufferTranscoder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/MemoryBufferTranscoder.cpp Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,141 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "MemoryBufferTranscoder.h" + +#include "../OrthancException.h" +#include "FromDcmtkBridge.h" + +namespace Orthanc +{ + MemoryBufferTranscoder::MemoryBufferTranscoder() + { +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + useDcmtk_ = true; +#else + useDcmtk_ = false; +#endif + } + + + void MemoryBufferTranscoder::SetDcmtkUsed(bool used) + { +#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1 + if (useDcmtk) + { + throw OrthancException(ErrorCode_NotImplemented, + "Orthanc was built without support for DMCTK transcoding"); + } +#endif + + useDcmtk_ = used; + } + + + bool MemoryBufferTranscoder::TranscodeParsedToBuffer( + std::string& target /* out */, + DicomTransferSyntax& sourceSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + DcmFileFormat& dicom /* in, possibly modified */, + DicomTransferSyntax targetSyntax, + bool allowNewSopInstanceUid) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string source; + FromDcmtkBridge::SaveToMemoryBuffer(source, *dicom.getDataset()); + + const void* data = source.empty() ? NULL : source.c_str(); + + std::set tmp; + tmp.insert(targetSyntax); + + DicomTransferSyntax targetSyntax2; + bool success = Transcode(target, sourceSyntax, targetSyntax2, hasSopInstanceUidChanged, + data, source.size(), tmp, allowNewSopInstanceUid); + + if (success && + targetSyntax != targetSyntax2) + { + throw OrthancException(ErrorCode_InternalError); + } + +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + if (!success && + useDcmtk_ && + dcmtk_.TranscodeParsedToBuffer( + target, sourceSyntax, hasSopInstanceUidChanged, + dicom, targetSyntax, allowNewSopInstanceUid)) + { + success = true; + } +#endif + + return success; + } + + + IDicomTranscoder::TranscodedDicom* MemoryBufferTranscoder::TranscodeToParsed( + DcmFileFormat& dicom /* in, possibly modified */, + const void* buffer /* in, same DICOM file as "dicom" */, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + DicomTransferSyntax sourceSyntax, targetSyntax; + bool hasSopInstanceUidChanged; + + std::string target; + if (Transcode(target, sourceSyntax, targetSyntax, hasSopInstanceUidChanged, + buffer, size, allowedSyntaxes, allowNewSopInstanceUid)) + { + const void* data = target.empty() ? NULL : target.c_str(); + return IDicomTranscoder::TranscodedDicom::CreateFromInternal( + FromDcmtkBridge::LoadFromMemoryBuffer(data, target.size()), hasSopInstanceUidChanged); + } +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + else if (useDcmtk_) + { + return dcmtk_.TranscodeToParsed(dicom, buffer, size, allowedSyntaxes, allowNewSopInstanceUid); + } +#endif + else + { + return NULL; + } + } +} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/MemoryBufferTranscoder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/MemoryBufferTranscoder.h Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,94 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING) +# error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file +#endif + +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 +# include "DcmtkTranscoder.h" +#endif + +namespace Orthanc +{ + // This is the basis class for transcoding plugins + class MemoryBufferTranscoder : public IDicomTranscoder + { + private: + bool useDcmtk_; + +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + DcmtkTranscoder dcmtk_; +#endif + + protected: + virtual bool Transcode(std::string& target, + DicomTransferSyntax& sourceSyntax /* out */, + DicomTransferSyntax& targetSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) = 0; + + public: + /** + * If "useDcmtk" is "true", the transcoder will first try and call + * DCMTK, before calling its own "Transcode()" implementation. + **/ + MemoryBufferTranscoder(); + + void SetDcmtkUsed(bool used); + + bool IsDcmtkUsed() const + { + return useDcmtk_; + } + + virtual bool TranscodeParsedToBuffer(std::string& target /* out */, + DicomTransferSyntax& sourceSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + DcmFileFormat& dicom /* in, possibly modified */, + DicomTransferSyntax targetSyntax, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + + virtual TranscodedDicom* TranscodeToParsed( + DcmFileFormat& dicom /* in, possibly modified */, + const void* buffer /* in, same DICOM file as "dicom" */, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + }; +} diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Tue May 12 14:58:24 2020 +0200 @@ -456,7 +456,7 @@ const UriComponents& uri) { DcmItem* dicom = pimpl_->file_->getDataset(); - E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer(); + E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getCurrentXfer(); // Special case: Accessing the pixel data if (uri.size() == 1 || @@ -869,7 +869,7 @@ std::string serialized; if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset())) { - output.AnswerBuffer(serialized, MimeType_Binary); + output.AnswerBuffer(serialized, MimeType_Dicom); } } #endif @@ -1113,6 +1113,12 @@ } + ParsedDicomFile::ParsedDicomFile(DcmFileFormat* dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(dicom); // No cloning + } + + DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const { return *pimpl_->file_.get(); @@ -1564,7 +1570,7 @@ pimpl_->frameIndex_->GetRawFrame(target, frameId); - E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer(); + E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getCurrentXfer(); switch (transferSyntax) { case EXS_JPEGProcess1: @@ -1625,7 +1631,22 @@ bool ParsedDicomFile::LookupTransferSyntax(std::string& result) { - return FromDcmtkBridge::LookupTransferSyntax(result, *pimpl_->file_); + // TODO - Shouldn't "dataset.getCurrentXfer()" be used instead of + // using the meta header? + const char* value = NULL; + + assert(pimpl_->file_ != NULL); + if (pimpl_->file_->getMetaInfo() != NULL && + pimpl_->file_->getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() && + value != NULL) + { + result.assign(value); + return true; + } + else + { + return false; + } } diff -r dba48c162b7b -r 82e88ff003d7 Core/DicomParsing/ParsedDicomFile.h --- a/Core/DicomParsing/ParsedDicomFile.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.h Tue May 12 14:58:24 2020 +0200 @@ -102,6 +102,8 @@ bool EmbedContentInternal(const std::string& dataUriScheme); + ParsedDicomFile(DcmFileFormat* dicom); // This takes ownership (no clone) + public: ParsedDicomFile(bool createIdentifiers); // Create a minimal DICOM instance @@ -114,9 +116,14 @@ ParsedDicomFile(const std::string& content); - ParsedDicomFile(DcmDataset& dicom); + ParsedDicomFile(DcmDataset& dicom); // This clones the DCMTK object + + ParsedDicomFile(DcmFileFormat& dicom); // This clones the DCMTK object - ParsedDicomFile(DcmFileFormat& dicom); + static ParsedDicomFile* AcquireDcmtkObject(DcmFileFormat* dicom) // No clone here + { + return new ParsedDicomFile(dicom); + } DcmFileFormat& GetDcmtkObject() const; diff -r dba48c162b7b -r 82e88ff003d7 Core/Enumerations.h --- a/Core/Enumerations.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/Enumerations.h Tue May 12 14:58:24 2020 +0200 @@ -280,13 +280,13 @@ DicomTransferSyntax_JPEG2000Multicomponent /*!< JPEG 2000 part 2 multicomponent extensions (lossless or lossy) */, DicomTransferSyntax_JPIPReferenced /*!< JPIP Referenced */, DicomTransferSyntax_JPIPReferencedDeflate /*!< JPIP Referenced Deflate */, - DicomTransferSyntax_MPEG2MainProfileAtMainLevel /*!< MPEG2 Main Profile at Main Level */, - DicomTransferSyntax_MPEG2MainProfileAtHighLevel /*!< MPEG2 Main Profile at High Level */, - DicomTransferSyntax_MPEG4HighProfileLevel4_1 /*!< MPEG4 High Profile / Level 4.1 */, - DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1 /*!< MPEG4 BD-compatible High Profile / Level 4.1 */, - DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo /*!< MPEG4 High Profile / Level 4.2 For 2D Video */, - DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo /*!< MPEG4 High Profile / Level 4.2 For 3D Video */, - DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2 /*!< 1.2.840.10008.1.2.4.106 */, + DicomTransferSyntax_MPEG2MainProfileAtMainLevel /*!< MPEG2 Main Profile / Main Level */, + DicomTransferSyntax_MPEG2MainProfileAtHighLevel /*!< MPEG2 Main Profile / High Level */, + DicomTransferSyntax_MPEG4HighProfileLevel4_1 /*!< MPEG4 AVC/H.264 High Profile / Level 4.1 */, + DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1 /*!< MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1 */, + DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video */, + DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video */, + DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2 /*!< MPEG4 AVC/H.264 Stereo High Profile / Level 4.2 */, DicomTransferSyntax_HEVCMainProfileLevel5_1 /*!< HEVC/H.265 Main Profile / Level 5.1 */, DicomTransferSyntax_HEVCMain10ProfileLevel5_1 /*!< HEVC/H.265 Main 10 Profile / Level 5.1 */, DicomTransferSyntax_RLELossless /*!< RLE - Run Length Encoding (lossless) */, @@ -754,7 +754,7 @@ DicomAssociationRole_Scu, DicomAssociationRole_Scp }; - + /** * WARNING: Do not change the explicit values in the enumerations diff -r dba48c162b7b -r 82e88ff003d7 Core/SerializationToolbox.cpp --- a/Core/SerializationToolbox.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Core/SerializationToolbox.cpp Tue May 12 14:58:24 2020 +0200 @@ -98,6 +98,21 @@ } + int ReadInteger(const Json::Value& value, + const std::string& field, + int defaultValue) + { + if (value.isMember(field.c_str())) + { + return ReadInteger(value, field); + } + else + { + return defaultValue; + } + } + + unsigned int ReadUnsignedInteger(const Json::Value& value, const std::string& field) { diff -r dba48c162b7b -r 82e88ff003d7 Core/SerializationToolbox.h --- a/Core/SerializationToolbox.h Tue May 12 07:50:38 2020 +0200 +++ b/Core/SerializationToolbox.h Tue May 12 14:58:24 2020 +0200 @@ -49,6 +49,10 @@ int ReadInteger(const Json::Value& value, const std::string& field); + int ReadInteger(const Json::Value& value, + const std::string& field, + int defaultValue); + unsigned int ReadUnsignedInteger(const Json::Value& value, const std::string& field); diff -r dba48c162b7b -r 82e88ff003d7 LinuxCompilation.txt --- a/LinuxCompilation.txt Tue May 12 07:50:38 2020 +0200 +++ b/LinuxCompilation.txt Tue May 12 14:58:24 2020 +0200 @@ -44,7 +44,7 @@ directory ("rm -rf ~/OrthancBuild") after installing this package, then run cmake again. -Note 3- To build the documentation, you will have to install doxyen. +Note 3- To build the documentation, you will have to install doxygen. Use system-wide libraries under GNU/Linux diff -r dba48c162b7b -r 82e88ff003d7 NEWS --- a/NEWS Tue May 12 07:50:38 2020 +0200 +++ b/NEWS Tue May 12 14:58:24 2020 +0200 @@ -4,10 +4,39 @@ REST API -------- +* API version has been upgraded to 7 * Improved: - "/instances/../modify": it is now possible to "Keep" the "SOPInstanceUID". Note that it was already possible to "Replace" it. + - added "Timeout" parameter to every DICOM operation + - "/queries/.../answers/../retrieve": "TargetAet" not mandatory anymore + (defaults to the local AET) +* Changes: + - "/instances/.../modify", ".../archive", ".../media", + "/tools/create-media" and "/tools/create-archive": New option "Transcode" + - "/ordered-slices": reverted the change introduced in 1.5.8 and go-back + to 1.5.7 behaviour. +Plugins +------- + +* New functions in the SDK: + - OrthancPluginCreateDicomInstance() + - OrthancPluginFreeDicomInstance() + - OrthancPluginGetInstanceFramesCount() + - OrthancPluginGetInstanceRawFrame() + - OrthancPluginGetInstanceDecodedFrame() + - OrthancPluginTranscodeDicomInstance() + - OrthancPluginSerializeDicomInstance() +* "OrthancPluginDicomInstance" structure wrapped in "OrthancPluginCppWrapper.h" + +Maintenance +----------- + +* Fix missing body in "OrthancPluginHttpPost()" and "OrthancPluginHttpPut()" +* Fix issue #169 (TransferSyntaxUID change from Explicit to Implicit during C-STORE SCU) +* Upgraded dependencies for static builds (notably on Windows and LSB): + - openssl 1.1.1g Version 1.6.1 (2020-04-21) diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/DicomInstanceToStore.cpp --- a/OrthancServer/DicomInstanceToStore.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/DicomInstanceToStore.cpp Tue May 12 14:58:24 2020 +0200 @@ -381,14 +381,14 @@ } - bool HasPixelData() + ParsedDicomFile& GetParsedDicomFile() { ComputeMissingInformation(); ParseDicomFile(); if (parsed_.HasContent()) { - return parsed_.GetContent().HasTag(DICOM_TAG_PIXEL_DATA); + return parsed_.GetContent(); } else { @@ -498,6 +498,11 @@ bool DicomInstanceToStore::HasPixelData() const { - return const_cast(*pimpl_).HasPixelData(); + return const_cast(*pimpl_).GetParsedDicomFile().HasTag(DICOM_TAG_PIXEL_DATA); + } + + ParsedDicomFile& DicomInstanceToStore::GetParsedDicomFile() const + { + return const_cast(*pimpl_).GetParsedDicomFile(); } } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/DicomInstanceToStore.h --- a/OrthancServer/DicomInstanceToStore.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/DicomInstanceToStore.h Tue May 12 14:58:24 2020 +0200 @@ -92,5 +92,7 @@ DicomInstanceHasher& GetHasher(); bool HasPixelData() const; + + ParsedDicomFile& GetParsedDicomFile() const; }; } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/IServerListener.h --- a/OrthancServer/IServerListener.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/IServerListener.h Tue May 12 14:58:24 2020 +0200 @@ -48,7 +48,7 @@ } virtual void SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) = 0; virtual void SignalChange(const ServerIndexChange& change) = 0; diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/LuaScripting.cpp --- a/OrthancServer/LuaScripting.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/LuaScripting.cpp Tue May 12 14:58:24 2020 +0200 @@ -588,7 +588,7 @@ } // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()" - return lock.AddStoreScuOperation(localAet, modality); + return lock.AddStoreScuOperation(context_, localAet, modality); } if (operation == "store-peer") @@ -821,7 +821,7 @@ void LuaScripting::SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) { Json::Value metadata = Json::objectValue; diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/LuaScripting.h --- a/OrthancServer/LuaScripting.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/LuaScripting.h Tue May 12 14:58:24 2020 +0200 @@ -121,7 +121,7 @@ void Stop(); void SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags); void SignalChange(const ServerIndexChange& change); diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Tue May 12 14:58:24 2020 +0200 @@ -59,7 +59,7 @@ RemoteModalityParameters remote_; std::string originatorAet_; uint16_t originatorId_; - std::unique_ptr connection_; + std::unique_ptr connection_; public: SynchronousMove(ServerContext& context, @@ -113,11 +113,15 @@ if (connection_.get() == NULL) { - connection_.reset(new DicomUserConnection(localAet_, remote_)); + DicomAssociationParameters params(localAet_, remote_); + connection_.reset(new DicomStoreUserConnection(params)); } std::string sopClassUid, sopInstanceUid; // Unused - connection_->Store(sopClassUid, sopInstanceUid, dicom, originatorAet_, originatorId_); + + const void* data = dicom.empty() ? NULL : dicom.c_str(); + connection_->Store(sopClassUid, sopInstanceUid, data, dicom.size(), + true, originatorAet_, originatorId_); return Status_Success; } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue May 12 14:58:24 2020 +0200 @@ -112,19 +112,45 @@ static void AnonymizeOrModifyInstance(DicomModification& modification, - RestApiPostCall& call) + RestApiPostCall& call, + bool transcode, + DicomTransferSyntax targetSyntax) { + ServerContext& context = OrthancRestApi::GetContext(call); std::string id = call.GetUriComponent("id", ""); std::unique_ptr modified; { - ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id); + ServerContext::DicomCacheLocker locker(context, id); modified.reset(locker.GetDicom().Clone(true)); } modification.Apply(*modified); - modified->Answer(call.GetOutput()); + + if (transcode) + { + std::string transcoded; + DicomTransferSyntax sourceSyntax; + bool hasSopInstanceUidChanged; + + if (context.GetTranscoder().TranscodeParsedToBuffer( + transcoded, sourceSyntax, hasSopInstanceUidChanged, + modified->GetDcmtkObject(), targetSyntax, true)) + { + call.GetOutput().AnswerBuffer(transcoded, MimeType_Dicom); + } + else + { + throw OrthancException(ErrorCode_InternalError, + "Cannot transcode to transfer syntax: " + + std::string(GetTransferSyntaxUid(targetSyntax))); + } + } + else + { + modified->Answer(call.GetOutput()); + } } @@ -153,7 +179,25 @@ modification.SetLevel(ResourceType_Instance); } - AnonymizeOrModifyInstance(modification, call); + if (request.isMember("Transcode")) + { + std::string s = SerializationToolbox::ReadString(request, "Transcode"); + + DicomTransferSyntax syntax; + if (LookupTransferSyntax(syntax, s)) + { + AnonymizeOrModifyInstance(modification, call, true, syntax); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s); + } + } + else + { + AnonymizeOrModifyInstance(modification, call, false /* no transcoding */, + DicomTransferSyntax_LittleEndianImplicit /* unused */); + } } @@ -165,7 +209,8 @@ Json::Value request; ParseAnonymizationRequest(request, modification, call); - AnonymizeOrModifyInstance(modification, call); + AnonymizeOrModifyInstance(modification, call, false /* no transcoding */, + DicomTransferSyntax_LittleEndianImplicit /* unused */); } @@ -227,7 +272,7 @@ toStore.SetParsedDicomFile(dicom); ServerContext& context = OrthancRestApi::GetContext(call); - StoreStatus status = context.Store(id, toStore); + StoreStatus status = context.Store(id, toStore, StoreInstanceMode_Default); if (status == StoreStatus_Failure) { @@ -236,7 +281,7 @@ if (sendAnswer) { - OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status); + OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, id); } } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Tue May 12 14:58:24 2020 +0200 @@ -63,11 +63,11 @@ void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call, DicomInstanceToStore& instance, - StoreStatus status) const + StoreStatus status, + const std::string& instanceId) const { Json::Value result; - SetupResourceAnswer(result, instance.GetHasher().HashInstance(), - ResourceType_Instance, status); + SetupResourceAnswer(result, instanceId, ResourceType_Instance, status); result["ParentPatient"] = instance.GetHasher().HashPatient(); result["ParentStudy"] = instance.GetHasher().HashStudy(); @@ -140,9 +140,9 @@ } std::string publicId; - StoreStatus status = context.Store(publicId, toStore); + StoreStatus status = context.Store(publicId, toStore, StoreInstanceMode_Default); - OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status); + OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, publicId); } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/OrthancRestApi/OrthancRestApi.h --- a/OrthancServer/OrthancRestApi/OrthancRestApi.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h Tue May 12 14:58:24 2020 +0200 @@ -107,9 +107,12 @@ static ServerIndex& GetIndex(RestApiCall& call); + // WARNING: "instanceId" can be different from + // "instance.GetHasher().HashInstance()" if transcoding is enabled void AnswerStoredInstance(RestApiPostCall& call, DicomInstanceToStore& instance, - StoreStatus status) const; + StoreStatus status, + const std::string& instanceId) const; void AnswerStoredResource(RestApiPostCall& call, const std::string& publicId, diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Tue May 12 14:58:24 2020 +0200 @@ -46,6 +46,7 @@ { static const char* const KEY_RESOURCES = "Resources"; static const char* const KEY_EXTENDED = "Extended"; + static const char* const KEY_TRANSCODE = "Transcode"; static void AddResourcesOfInterestFromArray(ArchiveJob& job, const Json::Value& resources) @@ -98,11 +99,28 @@ } - static void GetJobParameters(bool& synchronous, /* out */ - bool& extended, /* out */ - int& priority, /* out */ - const Json::Value& body, /* in */ - const bool defaultExtended /* in */) + static DicomTransferSyntax GetTransferSyntax(const std::string& value) + { + DicomTransferSyntax syntax; + if (LookupTransferSyntax(syntax, value)) + { + return syntax; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unknown transfer syntax: " + value); + } + } + + + static void GetJobParameters(bool& synchronous, /* out */ + bool& extended, /* out */ + bool& transcode, /* out */ + DicomTransferSyntax& syntax, /* out */ + int& priority, /* out */ + const Json::Value& body, /* in */ + const bool defaultExtended /* in */) { synchronous = OrthancRestApi::IsSynchronousJobRequest (true /* synchronous by default */, body); @@ -118,6 +136,17 @@ { extended = defaultExtended; } + + if (body.type() == Json::objectValue && + body.isMember(KEY_TRANSCODE)) + { + transcode = true; + syntax = GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE)); + } + else + { + transcode = false; + } } @@ -175,12 +204,20 @@ Json::Value body; if (call.ParseJsonRequest(body)) { - bool synchronous, extended; + bool synchronous, extended, transcode; + DicomTransferSyntax transferSyntax; int priority; - GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED); + GetJobParameters(synchronous, extended, transcode, transferSyntax, + priority, body, DEFAULT_IS_EXTENDED); std::unique_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); AddResourcesOfInterest(*job, body); + + if (transcode) + { + job->SetTranscode(transferSyntax); + } + SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip"); } else @@ -208,10 +245,16 @@ { extended = false; } - + std::unique_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); job->AddResource(id); + static const char* const TRANSCODE = "transcode"; + if (call.HasArgument(TRANSCODE)) + { + job->SetTranscode(GetTransferSyntax(call.GetArgument(TRANSCODE, ""))); + } + SubmitJob(call.GetOutput(), context, job, 0 /* priority */, true /* synchronous */, id + ".zip"); } @@ -228,12 +271,20 @@ Json::Value body; if (call.ParseJsonRequest(body)) { - bool synchronous, extended; + bool synchronous, extended, transcode; + DicomTransferSyntax transferSyntax; int priority; - GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED); + GetJobParameters(synchronous, extended, transcode, transferSyntax, + priority, body, DEFAULT_IS_EXTENDED); std::unique_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); job->AddResource(id); + + if (transcode) + { + job->SetTranscode(transferSyntax); + } + SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip"); } else diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Tue May 12 14:58:24 2020 +0200 @@ -54,12 +54,14 @@ namespace Orthanc { static const char* const KEY_LEVEL = "Level"; - static const char* const KEY_QUERY = "Query"; + static const char* const KEY_LOCAL_AET = "LocalAet"; static const char* const KEY_NORMALIZE = "Normalize"; + static const char* const KEY_QUERY = "Query"; static const char* const KEY_RESOURCES = "Resources"; + static const char* const KEY_TARGET_AET = "TargetAet"; + static const char* const KEY_TIMEOUT = "Timeout"; static const char* const SOP_CLASS_UID = "SOPClassUID"; static const char* const SOP_INSTANCE_UID = "SOPInstanceUID"; - static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name) { @@ -68,35 +70,62 @@ } + static void InjectAssociationTimeout(DicomAssociationParameters& params, + const Json::Value& body) + { + if (body.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object"); + } + else if (body.isMember(KEY_TIMEOUT)) + { + // New in Orthanc 1.7.0 + params.SetTimeout(SerializationToolbox::ReadUnsignedInteger(body, KEY_TIMEOUT)); + } + } + + static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call, + const Json::Value& body) + { + const std::string& localAet = + OrthancRestApi::GetContext(call).GetDefaultLocalApplicationEntityTitle(); + const RemoteModalityParameters remote = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + DicomAssociationParameters params(localAet, remote); + InjectAssociationTimeout(params, body); + + return params; + } + + + static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call) + { + Json::Value body; + call.ParseJsonRequest(body); + return GetAssociationParameters(call, body); + } + + /*************************************************************************** * DICOM C-Echo SCU ***************************************************************************/ static void DicomEcho(RestApiPostCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); + DicomControlUserConnection connection(GetAssociationParameters(call)); - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - - try + if (connection.Echo()) { - DicomControlUserConnection connection(localAet, remote); - - if (connection.Echo()) - { - // Echo has succeeded - call.GetOutput().AnswerBuffer("{}", MimeType_Json); - return; - } + // Echo has succeeded + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + return; } - catch (OrthancException&) + else { + // Echo has failed + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); } - - // Echo has failed - call.GetOutput().SignalError(HttpStatus_500_InternalServerError); } @@ -188,7 +217,6 @@ static void DicomFindPatient(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindPatientTemplate(fields); @@ -197,14 +225,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindPatient(answers, connection, fields); } @@ -216,7 +240,6 @@ static void DicomFindStudy(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindStudyTemplate(fields); @@ -231,14 +254,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindStudy(answers, connection, fields); } @@ -250,7 +269,6 @@ static void DicomFindSeries(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindSeriesTemplate(fields); @@ -266,14 +284,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindSeries(answers, connection, fields); } @@ -285,7 +299,6 @@ static void DicomFindInstance(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindInstanceTemplate(fields); @@ -302,14 +315,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindInstance(answers, connection, fields); } @@ -334,7 +343,6 @@ static void DicomFind(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap m; DicomMap::SetupFindPatientTemplate(m); @@ -343,11 +351,7 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); DicomFindAnswers patients(false); FindPatient(patients, connection, m); @@ -605,16 +609,25 @@ ServerContext& context = OrthancRestApi::GetContext(call); std::string targetAet; + int timeout = -1; Json::Value body; if (call.ParseJsonRequest(body)) { - targetAet = SerializationToolbox::ReadString(body, "TargetAet"); + targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); + timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1); } else { body = Json::objectValue; - call.BodyToString(targetAet); + if (call.GetBodySize() > 0) + { + call.BodyToString(targetAet); + } + else + { + targetAet = context.GetDefaultLocalApplicationEntityTitle(); + } } std::unique_ptr job(new DicomMoveScuJob(context)); @@ -625,6 +638,12 @@ job->SetLocalAet(query.GetHandler().GetLocalAet()); job->SetRemoteModality(query.GetHandler().GetRemoteModality()); + if (timeout >= 0) + { + // New in Orthanc 1.7.0 + job->SetTimeout(static_cast(timeout)); + } + LOG(WARNING) << "Driving C-Move SCU on remote modality " << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle() << " to target modality " << targetAet; @@ -948,7 +967,7 @@ GetInstancesToExport(request, *job, remote, call); std::string localAet = Toolbox::GetJsonStringField - (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); + (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle()); std::string moveOriginatorAET = Toolbox::GetJsonStringField (request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle()); int moveOriginatorID = Toolbox::GetJsonIntegerField @@ -968,6 +987,12 @@ job->EnableStorageCommitment(true); } + // New in Orthanc 1.7.0 + if (request.isMember(KEY_TIMEOUT)) + { + job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT)); + } + OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, request); } @@ -975,18 +1000,12 @@ static void DicomStoreStraight(RestApiPostCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); - - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - - DicomUserConnection connection(localAet, remote); - connection.Open(); + Json::Value body = Json::objectValue; // No body + DicomStoreUserConnection connection(GetAssociationParameters(call, body)); std::string sopClassUid, sopInstanceUid; - connection.Store(sopClassUid, sopInstanceUid, - call.GetBodyData(), call.GetBodySize()); + connection.Store(sopClassUid, sopInstanceUid, call.GetBodyData(), + call.GetBodySize(), false /* Not a C-MOVE */, "", 0); Json::Value answer = Json::objectValue; answer[SOP_CLASS_UID] = sopClassUid; @@ -1020,15 +1039,18 @@ ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); std::string localAet = Toolbox::GetJsonStringField - (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); + (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle()); std::string targetAet = Toolbox::GetJsonStringField - (request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle()); + (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); const RemoteModalityParameters source = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomControlUserConnection connection(localAet, source); - + DicomAssociationParameters params(localAet, source); + InjectAssociationTimeout(params, request); + + DicomControlUserConnection connection(params); + for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) { DicomMap resource; @@ -1294,15 +1316,9 @@ static void DicomFindWorklist(RestApiPostCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); - Json::Value json; if (call.ParseJsonRequest(json)) { - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - const RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - std::unique_ptr query (ParsedDicomFile::CreateFromJson(json, static_cast(0), "" /* no private creator */)); @@ -1310,7 +1326,7 @@ DicomFindAnswers answers(true); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call, json)); connection.FindWorklist(answers, *query); } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/QueryRetrieveHandler.cpp --- a/OrthancServer/QueryRetrieveHandler.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/QueryRetrieveHandler.cpp Tue May 12 14:58:24 2020 +0200 @@ -82,7 +82,8 @@ FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); { - DicomControlUserConnection connection(localAet_, modality_); + DicomAssociationParameters params(localAet_, modality_); + DicomControlUserConnection connection(params); connection.Find(answers_, level_, fixed, findNormalized_); } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerContext.cpp Tue May 12 14:58:24 2020 +0200 @@ -35,6 +35,7 @@ #include "ServerContext.h" #include "../Core/Cache/SharedArchive.h" +#include "../Core/DicomParsing/DcmtkTranscoder.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/FileStorage/StorageAccessor.h" #include "../Core/HttpServer/FilesystemHttpSender.h" @@ -242,7 +243,9 @@ isJobsEngineUnserialized_(false), metricsRegistry_(new MetricsRegistry), isHttpServerSecure_(true), - isExecuteLuaEnabled_(false) + isExecuteLuaEnabled_(false), + overwriteInstances_(false), + dcmtkTranscoder_(new DcmtkTranscoder) { { OrthancConfiguration::ReaderLock lock; @@ -263,6 +266,9 @@ // New configuration option in Orthanc 1.6.0 storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); + + // New option in Orthanc 1.7.0 + transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true); } jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); @@ -338,9 +344,29 @@ } - StoreStatus ServerContext::Store(std::string& resultPublicId, - DicomInstanceToStore& dicom) + StoreStatus ServerContext::StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode) { + bool overwrite; + switch (mode) + { + case StoreInstanceMode_Default: + overwrite = overwriteInstances_; + break; + + case StoreInstanceMode_OverwriteDuplicate: + overwrite = true; + break; + + case StoreInstanceMode_IgnoreDuplicate: + overwrite = false; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + try { MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms"); @@ -404,7 +430,8 @@ typedef std::map InstanceMetadata; InstanceMetadata instanceMetadata; - StoreStatus status = index_.Store(instanceMetadata, dicom, attachments); + StoreStatus status = index_.Store( + instanceMetadata, dicom, attachments, overwrite); // Only keep the metadata for the "instance" level dicom.GetMetadata().clear(); @@ -475,6 +502,62 @@ } + StoreStatus ServerContext::Store(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode) + { + //const DicomTransferSyntax option = DicomTransferSyntax_JPEGProcess1; + const DicomTransferSyntax option = DicomTransferSyntax_LittleEndianExplicit; + + if (1) + { + return StoreAfterTranscoding(resultPublicId, dicom, mode); + } + else + { + // TODO => Automated transcoding of incoming DICOM files + + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax( + sourceSyntax, dicom.GetParsedDicomFile().GetDcmtkObject()) || + sourceSyntax == option) + { + // No transcoding + return StoreAfterTranscoding(resultPublicId, dicom, mode); + } + else + { + std::set syntaxes; + syntaxes.insert(option); + + std::unique_ptr transcoded( + GetTranscoder().TranscodeToParsed(dicom.GetParsedDicomFile().GetDcmtkObject(), + dicom.GetBufferData(), dicom.GetBufferSize(), + syntaxes, true /* allow new SOP instance UID */)); + + if (transcoded.get() == NULL) + { + // Cannot transcode => store the original file + return StoreAfterTranscoding(resultPublicId, dicom, mode); + } + else + { + std::unique_ptr tmp( + ParsedDicomFile::AcquireDcmtkObject(transcoded->ReleaseDicom())); + + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(*tmp); + toStore.SetOrigin(dicom.GetOrigin()); + + StoreStatus ok = StoreAfterTranscoding(resultPublicId, toStore, mode); + printf(">> %s\n", resultPublicId.c_str()); + return ok; + } + } + } + } + + void ServerContext::AnswerAttachment(RestApiOutput& output, const std::string& resourceId, FileContentType content) @@ -1086,4 +1169,50 @@ return NULL; } + + + IDicomTranscoder& ServerContext::GetTranscoder() + { + IDicomTranscoder* transcoder = dcmtkTranscoder_.get(); + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins()) + { + transcoder = &GetPlugins(); + } +#endif + + if (transcoder == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + return *transcoder; + } + } + + + void ServerContext::StoreWithTranscoding(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomStoreUserConnection& connection, + const std::string& dicom, + bool hasMoveOriginator, + const std::string& moveOriginatorAet, + uint16_t moveOriginatorId) + { + const void* data = dicom.empty() ? NULL : dicom.c_str(); + + if (!transcodeDicomProtocol_ || + !connection.GetParameters().GetRemoteModality().IsTranscodingAllowed()) + { + connection.Store(sopClassUid, sopInstanceUid, data, dicom.size(), + hasMoveOriginator, moveOriginatorAet, moveOriginatorId); + } + else + { + connection.Transcode(sopClassUid, sopInstanceUid, GetTranscoder(), data, dicom.size(), + hasMoveOriginator, moveOriginatorAet, moveOriginatorId); + } + } } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerContext.h Tue May 12 14:58:24 2020 +0200 @@ -40,6 +40,7 @@ #include "ServerJobs/IStorageCommitmentFactory.h" #include "../Core/Cache/MemoryCache.h" +#include "../Core/DicomParsing/IDicomTranscoder.h" namespace Orthanc @@ -98,7 +99,7 @@ } virtual void SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) { context_.mainLua_.SignalStoredInstance(publicId, instance, simplifiedTags); @@ -221,9 +222,17 @@ std::unique_ptr metricsRegistry_; bool isHttpServerSecure_; bool isExecuteLuaEnabled_; + bool overwriteInstances_; std::unique_ptr storageCommitmentReports_; + bool transcodeDicomProtocol_; + std::unique_ptr dcmtkTranscoder_; + + StoreStatus StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode); + public: class DicomCacheLocker : public boost::noncopyable { @@ -275,7 +284,8 @@ size_t size); StoreStatus Store(std::string& resultPublicId, - DicomInstanceToStore& dicom); + DicomInstanceToStore& dicom, + StoreInstanceMode mode); void AnswerAttachment(RestApiOutput& output, const std::string& resourceId, @@ -426,6 +436,16 @@ return isExecuteLuaEnabled_; } + void SetOverwriteInstances(bool overwrite) + { + overwriteInstances_ = overwrite; + } + + bool IsOverwriteInstances() const + { + return overwriteInstances_; + } + virtual IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment(const std::string& jobId, const std::string& transactionUid, @@ -438,5 +458,17 @@ { return *storageCommitmentReports_; } + + void StoreWithTranscoding(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomStoreUserConnection& connection, + const std::string& dicom, + bool hasMoveOriginator, + const std::string& moveOriginatorAet, + uint16_t moveOriginatorId); + + // This accessor can be used even if the global option + // "TranscodeDicomProtocol" is set to "false" + IDicomTranscoder& GetTranscoder(); }; } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerEnumerations.h Tue May 12 14:58:24 2020 +0200 @@ -90,6 +90,13 @@ FindStorageAccessMode_DiskOnLookupAndAnswer }; + enum StoreInstanceMode + { + StoreInstanceMode_Default, + StoreInstanceMode_OverwriteDuplicate, + StoreInstanceMode_IgnoreDuplicate + }; + /** * WARNING: Do not change the explicit values in the enumerations diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerIndex.cpp Tue May 12 14:58:24 2020 +0200 @@ -675,7 +675,6 @@ db_(db), maximumStorageSize_(0), maximumPatients_(0), - overwrite_(false), mainDicomTagsRegistry_(new MainDicomTagsRegistry) { listener_.reset(new Listener(context)); @@ -753,7 +752,8 @@ StoreStatus ServerIndex::Store(std::map& instanceMetadata, DicomInstanceToStore& instanceToStore, - const Attachments& attachments) + const Attachments& attachments, + bool overwrite) { boost::mutex::scoped_lock lock(mutex_); @@ -784,7 +784,7 @@ { // The instance already exists - if (overwrite_) + if (overwrite) { // Overwrite the old instance LOG(INFO) << "Overwriting instance: " << hashInstance; @@ -1660,12 +1660,6 @@ StandaloneRecycling(); } - void ServerIndex::SetOverwriteInstances(bool overwrite) - { - boost::mutex::scoped_lock lock(mutex_); - overwrite_ = overwrite; - } - void ServerIndex::StandaloneRecycling() { diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerIndex.h Tue May 12 14:58:24 2020 +0200 @@ -71,7 +71,6 @@ uint64_t maximumStorageSize_; unsigned int maximumPatients_; - bool overwrite_; std::unique_ptr mainDicomTagsRegistry_; static void FlushThread(ServerIndex* that, @@ -139,11 +138,10 @@ // "count == 0" means no limit on the number of patients void SetMaximumPatientCount(unsigned int count); - void SetOverwriteInstances(bool overwrite); - StoreStatus Store(std::map& instanceMetadata, DicomInstanceToStore& instance, - const Attachments& attachments); + const Attachments& attachments, + bool overwrite); void GetGlobalStatistics(/* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/ServerJobs/ArchiveJob.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Tue May 12 14:58:24 2020 +0200 @@ -37,6 +37,7 @@ #include "../../Core/Cache/SharedArchive.h" #include "../../Core/Compression/HierarchicalZipWriter.h" #include "../../Core/DicomParsing/DicomDirWriter.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" #include "../OrthancConfiguration.h" @@ -55,6 +56,7 @@ static const char* const KEY_DESCRIPTION = "Description"; static const char* const KEY_INSTANCES_COUNT = "InstancesCount"; static const char* const KEY_UNCOMPRESSED_SIZE_MB = "UncompressedSizeMB"; +static const char* const KEY_TRANSCODE = "Transcode"; namespace Orthanc @@ -399,7 +401,9 @@ void Apply(HierarchicalZipWriter& writer, ServerContext& context, DicomDirWriter* dicomDir, - const std::string& dicomDirFolder) const + const std::string& dicomDirFolder, + bool transcode, + DicomTransferSyntax transferSyntax) const { switch (type_) { @@ -426,14 +430,65 @@ } //boost::this_thread::sleep(boost::posix_time::milliseconds(300)); - + writer.OpenFile(filename_.c_str()); - writer.Write(content); + + bool transcodeSuccess = false; + + std::unique_ptr parsed; + + if (transcode) + { + // New in Orthanc 1.7.0 + std::set syntaxes; + syntaxes.insert(transferSyntax); + + parsed.reset(new ParsedDicomFile(content)); + const char* data = content.empty() ? NULL : content.c_str(); + + std::unique_ptr transcodedDicom( + context.GetTranscoder().TranscodeToParsed( + parsed->GetDcmtkObject(), data, content.size(), + syntaxes, true /* allow new SOP instance UID */)); + + if (transcodedDicom.get() != NULL && + transcodedDicom->GetDicom().getDataset() != NULL) + { + std::string transcoded; + FromDcmtkBridge::SaveToMemoryBuffer( + transcoded, *transcodedDicom->GetDicom().getDataset()); + + writer.Write(transcoded); - if (dicomDir != NULL) + if (dicomDir != NULL) + { + std::unique_ptr tmp( + ParsedDicomFile::AcquireDcmtkObject(transcodedDicom->ReleaseDicom())); + dicomDir->Add(dicomDirFolder, filename_, *tmp); + } + + transcodeSuccess = true; + } + else + { + LOG(INFO) << "Cannot transcode instance " << instanceId_ + << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax); + } + } + + if (!transcodeSuccess) { - ParsedDicomFile parsed(content); - dicomDir->Add(dicomDirFolder, filename_, parsed); + writer.Write(content); + + if (dicomDir != NULL) + { + if (parsed.get() == NULL) + { + parsed.reset(new ParsedDicomFile(content)); + } + + dicomDir->Add(dicomDirFolder, filename_, *parsed); + } } break; @@ -454,14 +509,16 @@ ServerContext& context, size_t index, DicomDirWriter* dicomDir, - const std::string& dicomDirFolder) const + const std::string& dicomDirFolder, + bool transcode, + DicomTransferSyntax transferSyntax) const { if (index >= commands_.size()) { throw OrthancException(ErrorCode_ParameterOutOfRange); } - commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder); + commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder, transcode, transferSyntax); } public: @@ -496,20 +553,26 @@ return uncompressedSize_; } + // "media" flavor (with DICOMDIR) void Apply(HierarchicalZipWriter& writer, ServerContext& context, size_t index, DicomDirWriter& dicomDir, - const std::string& dicomDirFolder) const + const std::string& dicomDirFolder, + bool transcode, + DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder); + ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder, transcode, transferSyntax); } + // "archive" flavor (without DICOMDIR) void Apply(HierarchicalZipWriter& writer, ServerContext& context, - size_t index) const + size_t index, + bool transcode, + DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, context, index, NULL, ""); + ApplyInternal(writer, context, index, NULL, "", transcode, transferSyntax); } void AddOpenDirectory(const std::string& filename) @@ -740,7 +803,9 @@ return commands_.GetSize() + 1; } - void RunStep(size_t index) + void RunStep(size_t index, + bool transcode, + DicomTransferSyntax transferSyntax) { if (index > commands_.GetSize()) { @@ -764,12 +829,13 @@ if (isMedia_) { assert(dicomDir_.get() != NULL); - commands_.Apply(*zip_, context_, index, *dicomDir_, MEDIA_IMAGES_FOLDER); + commands_.Apply(*zip_, context_, index, *dicomDir_, + MEDIA_IMAGES_FOLDER, transcode, transferSyntax); } else { assert(dicomDir_.get() == NULL); - commands_.Apply(*zip_, context_, index); + commands_.Apply(*zip_, context_, index, transcode, transferSyntax); } } } @@ -795,7 +861,9 @@ enableExtendedSopClass_(enableExtendedSopClass), currentStep_(0), instancesCount_(0), - uncompressedSize_(0) + uncompressedSize_(0), + transcode_(false), + transferSyntax_(DicomTransferSyntax_LittleEndianImplicit) { } @@ -854,6 +922,20 @@ } } + + void ArchiveJob::SetTranscode(DicomTransferSyntax transferSyntax) + { + if (writer_.get() != NULL) // Already started + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = true; + transferSyntax_ = transferSyntax; + } + } + void ArchiveJob::Reset() { @@ -954,7 +1036,7 @@ } else { - writer_->RunStep(currentStep_); + writer_->RunStep(currentStep_, transcode_, transferSyntax_); currentStep_ ++; @@ -1006,6 +1088,11 @@ value[KEY_INSTANCES_COUNT] = instancesCount_; value[KEY_UNCOMPRESSED_SIZE_MB] = static_cast(uncompressedSize_ / MEGA_BYTES); + + if (transcode_) + { + value[KEY_TRANSCODE] = GetTransferSyntaxUid(transferSyntax_); + } } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/ArchiveJob.h --- a/OrthancServer/ServerJobs/ArchiveJob.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.h Tue May 12 14:58:24 2020 +0200 @@ -69,6 +69,10 @@ uint64_t uncompressedSize_; std::string mediaArchiveId_; + // New in Orthanc 1.7.0 + bool transcode_; + DicomTransferSyntax transferSyntax_; + void FinalizeTarget(); public: @@ -89,6 +93,8 @@ void AddResource(const std::string& publicId); + void SetTranscode(DicomTransferSyntax transferSyntax); + virtual void Reset() ORTHANC_OVERRIDE; virtual void Start() ORTHANC_OVERRIDE; diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/DicomModalityStoreJob.cpp --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Tue May 12 14:58:24 2020 +0200 @@ -35,6 +35,7 @@ #include "DicomModalityStoreJob.h" #include "../../Core/Compatibility.h" +#include "../../Core/DicomNetworking/DicomAssociation.h" #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" @@ -47,7 +48,7 @@ { if (connection_.get() == NULL) { - connection_.reset(new DicomUserConnection(localAet_, remote_)); + connection_.reset(new DicomStoreUserConnection(parameters_)); } } @@ -58,7 +59,7 @@ OpenConnection(); LOG(INFO) << "Sending instance " << instance << " to modality \"" - << remote_.GetApplicationEntityTitle() << "\""; + << parameters_.GetRemoteModality().GetApplicationEntityTitle() << "\""; std::string dicom; @@ -73,15 +74,8 @@ } std::string sopClassUid, sopInstanceUid; - - if (HasMoveOriginator()) - { - connection_->Store(sopClassUid, sopInstanceUid, dicom, moveOriginatorAet_, moveOriginatorId_); - } - else - { - connection_->Store(sopClassUid, sopInstanceUid, dicom); - } + context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom, + HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_); if (storageCommitment_) { @@ -96,7 +90,10 @@ if (sopClassUids_.size() == GetInstancesCount()) { - const std::string& remoteAet = remote_.GetApplicationEntityTitle(); + assert(IsStarted()); + connection_.reset(NULL); + + const std::string& remoteAet = parameters_.GetRemoteModality().GetApplicationEntityTitle(); LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet; @@ -105,12 +102,10 @@ context_.GetStorageCommitmentReports().Store( transactionUid_, new StorageCommitmentReports::Report(remoteAet)); - assert(IsStarted()); - OpenConnection(); - std::vector a(sopClassUids_.begin(), sopClassUids_.end()); std::vector b(sopInstanceUids_.begin(), sopInstanceUids_.end()); - connection_->RequestStorageCommitment(transactionUid_, a, b); + + DicomAssociation::RequestStorageCommitment(parameters_, transactionUid_, a, b); } } @@ -128,7 +123,6 @@ DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) : context_(context), - localAet_("ORTHANC"), moveOriginatorId_(0), // By default, not a C-MOVE storageCommitment_(false) // By default, no storage commitment { @@ -144,7 +138,7 @@ } else { - localAet_ = aet; + parameters_.SetLocalApplicationEntityTitle(aet); } } @@ -157,11 +151,24 @@ } else { - remote_ = remote; + parameters_.SetRemoteModality(remote); } } + void DicomModalityStoreJob::SetTimeout(uint32_t seconds) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetTimeout(seconds); + } + } + + const std::string& DicomModalityStoreJob::GetMoveOriginatorAet() const { if (HasMoveOriginator()) @@ -249,8 +256,8 @@ { SetOfInstancesJob::GetPublicContent(value); - value["LocalAet"] = localAet_; - value["RemoteAet"] = remote_.GetApplicationEntityTitle(); + value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle(); + value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle(); if (HasMoveOriginator()) { @@ -265,8 +272,6 @@ } - static const char* LOCAL_AET = "LocalAet"; - static const char* REMOTE = "Remote"; static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet"; static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId"; static const char* STORAGE_COMMITMENT = "StorageCommitment"; @@ -277,12 +282,12 @@ SetOfInstancesJob(serialized), context_(context) { - localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); - remote_ = RemoteModalityParameters(serialized[REMOTE]); moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET); moveOriginatorId_ = static_cast (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID)); EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT)); + + parameters_ = DicomAssociationParameters::UnserializeJob(serialized); } @@ -294,8 +299,7 @@ } else { - target[LOCAL_AET] = localAet_; - remote_.Serialize(target[REMOTE], true /* force advanced format */); + parameters_.SerializeJob(target); target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_; target[MOVE_ORIGINATOR_ID] = moveOriginatorId_; target[STORAGE_COMMITMENT] = storageCommitment_; diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/DicomModalityStoreJob.h --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h Tue May 12 14:58:24 2020 +0200 @@ -35,7 +35,9 @@ #include "../../Core/Compatibility.h" #include "../../Core/JobsEngine/SetOfInstancesJob.h" -#include "../../Core/DicomNetworking/DicomUserConnection.h" +#include "../../Core/DicomNetworking/DicomStoreUserConnection.h" + +#include namespace Orthanc { @@ -44,13 +46,12 @@ class DicomModalityStoreJob : public SetOfInstancesJob { private: - ServerContext& context_; - std::string localAet_; - RemoteModalityParameters remote_; - std::string moveOriginatorAet_; - uint16_t moveOriginatorId_; - std::unique_ptr connection_; - bool storageCommitment_; + ServerContext& context_; + DicomAssociationParameters parameters_; + std::string moveOriginatorAet_; + uint16_t moveOriginatorId_; + std::unique_ptr connection_; + bool storageCommitment_; // For storage commitment std::string transactionUid_; @@ -72,19 +73,16 @@ DicomModalityStoreJob(ServerContext& context, const Json::Value& serialized); - const std::string& GetLocalAet() const + const DicomAssociationParameters& GetParameters() const { - return localAet_; + return parameters_; } void SetLocalAet(const std::string& aet); - const RemoteModalityParameters& GetRemoteModality() const - { - return remote_; - } + void SetRemoteModality(const RemoteModalityParameters& remote); - void SetRemoteModality(const RemoteModalityParameters& remote); + void SetTimeout(uint32_t seconds); bool HasMoveOriginator() const { @@ -112,5 +110,10 @@ virtual void Reset() ORTHANC_OVERRIDE; void EnableStorageCommitment(bool enabled); + + bool HasStorageCommitment() const + { + return storageCommitment_; + } }; } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/DicomMoveScuJob.cpp --- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Tue May 12 14:58:24 2020 +0200 @@ -40,6 +40,7 @@ static const char* const TARGET_AET = "TargetAet"; static const char* const REMOTE = "Remote"; static const char* const QUERY = "Query"; +static const char* const TIMEOUT = "Timeout"; namespace Orthanc { @@ -96,7 +97,7 @@ { if (connection_.get() == NULL) { - connection_.reset(new DicomControlUserConnection(localAet_, remote_)); + connection_.reset(new DicomControlUserConnection(parameters_)); } connection_->Move(targetAet_, findAnswer); @@ -152,7 +153,7 @@ } else { - localAet_ = aet; + parameters_.SetLocalApplicationEntityTitle(aet); } } @@ -178,7 +179,20 @@ } else { - remote_ = remote; + parameters_.SetRemoteModality(remote); + } + } + + + void DicomMoveScuJob::SetTimeout(uint32_t seconds) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetTimeout(seconds); } } @@ -192,9 +206,9 @@ void DicomMoveScuJob::GetPublicContent(Json::Value& value) { SetOfCommandsJob::GetPublicContent(value); - - value["LocalAet"] = localAet_; - value["RemoteAet"] = remote_.GetApplicationEntityTitle(); + + value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle(); + value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle(); value["Query"] = query_; } @@ -205,9 +219,8 @@ context_(context), query_(Json::arrayValue) { - localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); + parameters_ = DicomAssociationParameters::UnserializeJob(serialized); targetAet_ = SerializationToolbox::ReadString(serialized, TARGET_AET); - remote_ = RemoteModalityParameters(serialized[REMOTE]); if (serialized.isMember(QUERY) && serialized[QUERY].type() == Json::arrayValue) @@ -225,10 +238,9 @@ } else { - target[LOCAL_AET] = localAet_; + parameters_.SerializeJob(target); target[TARGET_AET] = targetAet_; target[QUERY] = query_; - remote_.Serialize(target[REMOTE], true /* force advanced format */); return true; } } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/DicomMoveScuJob.h --- a/OrthancServer/ServerJobs/DicomMoveScuJob.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.h Tue May 12 14:58:24 2020 +0200 @@ -49,11 +49,10 @@ class Command; class Unserializer; - ServerContext& context_; - std::string localAet_; - std::string targetAet_; - RemoteModalityParameters remote_; - Json::Value query_; + ServerContext& context_; + DicomAssociationParameters parameters_; + std::string targetAet_; + Json::Value query_; std::unique_ptr connection_; @@ -73,28 +72,25 @@ void AddFindAnswer(QueryRetrieveHandler& query, size_t i); - - const std::string& GetLocalAet() const + + const DicomAssociationParameters& GetParameters() const { - return localAet_; + return parameters_; } + + void SetLocalAet(const std::string& aet); - void SetLocalAet(const std::string& aet); + void SetRemoteModality(const RemoteModalityParameters& remote); + + void SetTimeout(uint32_t timeout); const std::string& GetTargetAet() const { return targetAet_; } - + void SetTargetAet(const std::string& aet); - const RemoteModalityParameters& GetRemoteModality() const - { - return remote_; - } - - void SetRemoteModality(const RemoteModalityParameters& remote); - virtual void Stop(JobStopReason reason); virtual void GetJobType(std::string& target) diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/LuaJobManager.cpp --- a/OrthancServer/ServerJobs/LuaJobManager.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/LuaJobManager.cpp Tue May 12 14:58:24 2020 +0200 @@ -200,11 +200,13 @@ } - size_t LuaJobManager::Lock::AddStoreScuOperation(const std::string& localAet, + size_t LuaJobManager::Lock::AddStoreScuOperation(ServerContext& context, + const std::string& localAet, const RemoteModalityParameters& modality) { assert(jobLock_.get() != NULL); - return jobLock_->AddOperation(new StoreScuOperation(that_.connectionManager_, localAet, modality)); + return jobLock_->AddOperation(new StoreScuOperation( + context, that_.connectionManager_, localAet, modality)); } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/LuaJobManager.h --- a/OrthancServer/ServerJobs/LuaJobManager.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/LuaJobManager.h Tue May 12 14:58:24 2020 +0200 @@ -91,7 +91,8 @@ size_t AddDeleteResourceOperation(ServerContext& context); - size_t AddStoreScuOperation(const std::string& localAet, + size_t AddStoreScuOperation(ServerContext& context, + const std::string& localAet, const RemoteModalityParameters& modality); size_t AddStorePeerOperation(const WebServiceParameters& peer); diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/MergeStudyJob.cpp --- a/OrthancServer/ServerJobs/MergeStudyJob.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp Tue May 12 14:58:24 2020 +0200 @@ -145,7 +145,8 @@ toStore.SetParsedDicomFile(*modified); std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + if (context_.Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { LOG(ERROR) << "Error while storing a modified instance " << instance; return false; diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp --- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Tue May 12 14:58:24 2020 +0200 @@ -113,7 +113,7 @@ toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, instance.GetId()); std::string modifiedId; - context_.Store(modifiedId, toStore); + context_.Store(modifiedId, toStore, StoreInstanceMode_Default); // Only chain with other commands if this command succeeds outputs.Append(new DicomInstanceOperationValue(instance.GetServerContext(), modifiedId)); diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp --- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Tue May 12 14:58:24 2020 +0200 @@ -35,6 +35,7 @@ #include "StoreScuOperation.h" #include "DicomInstanceOperationValue.h" +#include "../../ServerContext.h" #include "../../../Core/Logging.h" #include "../../../Core/OrthancException.h" @@ -64,7 +65,8 @@ instance.ReadDicom(dicom); std::string sopClassUid, sopInstanceUid; // Unused - lock.GetConnection().Store(sopClassUid, sopInstanceUid, dicom); + context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom, + false /* Not a C-MOVE */, "", 0); } catch (OrthancException& e) { @@ -85,8 +87,10 @@ } - StoreScuOperation::StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + StoreScuOperation::StoreScuOperation(ServerContext& context, + TimeoutDicomConnectionManager& connectionManager, const Json::Value& serialized) : + context_(context), connectionManager_(connectionManager) { if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" || diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/Operations/StoreScuOperation.h --- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.h Tue May 12 14:58:24 2020 +0200 @@ -38,24 +38,30 @@ namespace Orthanc { + class ServerContext; + class StoreScuOperation : public IJobOperation { private: + ServerContext& context_; TimeoutDicomConnectionManager& connectionManager_; std::string localAet_; RemoteModalityParameters modality_; public: - StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + StoreScuOperation(ServerContext& context, + TimeoutDicomConnectionManager& connectionManager, const std::string& localAet, const RemoteModalityParameters& modality) : + context_(context), connectionManager_(connectionManager), localAet_(localAet), modality_(modality) { } - StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + StoreScuOperation(ServerContext& context, + TimeoutDicomConnectionManager& connectionManager, const Json::Value& serialized); const std::string& GetLocalAet() const diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/OrthancJobUnserializer.cpp --- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Tue May 12 14:58:24 2020 +0200 @@ -127,7 +127,7 @@ else if (type == "StoreScu") { return new StoreScuOperation( - context_.GetLuaScripting().GetDicomConnectionManager(), source); + context_, context_.GetLuaScripting().GetDicomConnectionManager(), source); } else if (type == "SystemCall") { diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp Tue May 12 14:58:24 2020 +0200 @@ -211,7 +211,8 @@ **/ std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + if (context_.Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { throw OrthancException(ErrorCode_CannotStoreInstance, "Error while storing a modified instance " + instance); diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/ServerJobs/SplitStudyJob.cpp --- a/OrthancServer/ServerJobs/SplitStudyJob.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp Tue May 12 14:58:24 2020 +0200 @@ -138,7 +138,8 @@ toStore.SetParsedDicomFile(*modified); std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + if (context_.Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { LOG(ERROR) << "Error while storing a modified instance " << instance; return false; diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/SliceOrdering.cpp --- a/OrthancServer/SliceOrdering.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/SliceOrdering.cpp Tue May 12 14:58:24 2020 +0200 @@ -320,7 +320,6 @@ if (instances_.size() <= 1) { // One single instance: It is sorted by default - sortedInstances_ = instances_; return true; } @@ -329,32 +328,25 @@ return false; } - sortedInstances_.clear(); - - // consider only the instances with a position and correctly oriented (if they have a normal) for (size_t i = 0; i < instances_.size(); i++) { assert(instances_[i] != NULL); - if (instances_[i]->HasPosition() && - (!instances_[i]->HasNormal() || - IsParallelOrOpposite(instances_[i]->GetNormal(), normal_))) + + if (!instances_[i]->HasPosition() || + (instances_[i]->HasNormal() && + !IsParallelOrOpposite(instances_[i]->GetNormal(), normal_))) { - sortedInstances_.push_back(instances_[i]); + return false; } } - if (sortedInstances_.size() == 0) - { - return false; - } + PositionComparator comparator(normal_); + std::sort(instances_.begin(), instances_.end(), comparator); - PositionComparator comparator(normal_); - std::sort(sortedInstances_.begin(), sortedInstances_.end(), comparator); - - float a = sortedInstances_[0]->ComputeRelativePosition(normal_); - for (size_t i = 1; i < sortedInstances_.size(); i++) + float a = instances_[0]->ComputeRelativePosition(normal_); + for (size_t i = 1; i < instances_.size(); i++) { - float b = sortedInstances_[i]->ComputeRelativePosition(normal_); + float b = instances_[i]->ComputeRelativePosition(normal_); if (std::fabs(b - a) <= 10.0f * std::numeric_limits::epsilon()) { @@ -376,38 +368,27 @@ if (instances_.size() <= 1) { // One single instance: It is sorted by default - sortedInstances_ = instances_; return true; } - sortedInstances_.clear(); - - // consider only the instances with an index for (size_t i = 0; i < instances_.size(); i++) { assert(instances_[i] != NULL); - if (instances_[i]->HasIndexInSeries()) + if (!instances_[i]->HasIndexInSeries()) { - sortedInstances_.push_back(instances_[i]); + return false; } } - if (sortedInstances_.size() == 0) // if we were not able to sort instances because none of them had an index, return all instances in a "random" order - { - sortedInstances_ = instances_; - } - else + std::sort(instances_.begin(), instances_.end(), IndexInSeriesComparator); + + for (size_t i = 1; i < instances_.size(); i++) { - std::sort(sortedInstances_.begin(), sortedInstances_.end(), IndexInSeriesComparator); - - for (size_t i = 1; i < sortedInstances_.size(); i++) + if (instances_[i - 1]->GetIndexInSeries() == instances_[i]->GetIndexInSeries()) { - if (sortedInstances_[i - 1]->GetIndexInSeries() == sortedInstances_[i]->GetIndexInSeries()) - { - // The current "IndexInSeries" occurs 2 times: Not a proper ordering - LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway"; - break; - } + // The current "IndexInSeries" occurs 2 times: Not a proper ordering + LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway"; + break; } } @@ -446,28 +427,28 @@ } - const std::string& SliceOrdering::GetSortedInstanceId(size_t index) const + const std::string& SliceOrdering::GetInstanceId(size_t index) const { - if (index >= sortedInstances_.size()) + if (index >= instances_.size()) { throw OrthancException(ErrorCode_ParameterOutOfRange); } else { - return sortedInstances_[index]->GetIdentifier(); + return instances_[index]->GetIdentifier(); } } - unsigned int SliceOrdering::GetSortedInstanceFramesCount(size_t index) const + unsigned int SliceOrdering::GetFramesCount(size_t index) const { - if (index >= sortedInstances_.size()) + if (index >= instances_.size()) { throw OrthancException(ErrorCode_ParameterOutOfRange); } else { - return sortedInstances_[index]->GetFramesCount(); + return instances_[index]->GetFramesCount(); } } @@ -478,9 +459,9 @@ result["Type"] = (isVolume_ ? "Volume" : "Sequence"); Json::Value tmp = Json::arrayValue; - for (size_t i = 0; i < GetSortedInstancesCount(); i++) + for (size_t i = 0; i < GetInstancesCount(); i++) { - tmp.append(GetBasePath(ResourceType_Instance, GetSortedInstanceId(i)) + "/file"); + tmp.append(GetBasePath(ResourceType_Instance, GetInstanceId(i)) + "/file"); } result["Dicom"] = tmp; @@ -488,18 +469,18 @@ Json::Value slicesShort = Json::arrayValue; tmp.clear(); - for (size_t i = 0; i < GetSortedInstancesCount(); i++) + for (size_t i = 0; i < GetInstancesCount(); i++) { - std::string base = GetBasePath(ResourceType_Instance, GetSortedInstanceId(i)); - for (size_t j = 0; j < GetSortedInstanceFramesCount(i); j++) + std::string base = GetBasePath(ResourceType_Instance, GetInstanceId(i)); + for (size_t j = 0; j < GetFramesCount(i); j++) { tmp.append(base + "/frames/" + boost::lexical_cast(j)); } Json::Value tmp2 = Json::arrayValue; - tmp2.append(GetSortedInstanceId(i)); + tmp2.append(GetInstanceId(i)); tmp2.append(0); - tmp2.append(GetSortedInstanceFramesCount(i)); + tmp2.append(GetFramesCount(i)); slicesShort.append(tmp2); } diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/SliceOrdering.h --- a/OrthancServer/SliceOrdering.h Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/SliceOrdering.h Tue May 12 14:58:24 2020 +0200 @@ -51,8 +51,7 @@ std::string seriesId_; bool hasNormal_; Vector normal_; - std::vector instances_; // this vector owns the instances - std::vector sortedInstances_; // this vectore references the instances of instances_ + std::vector instances_; bool isVolume_; static bool ComputeNormal(Vector& normal, @@ -78,14 +77,14 @@ ~SliceOrdering(); - size_t GetSortedInstancesCount() const + size_t GetInstancesCount() const { - return sortedInstances_.size(); + return instances_.size(); } - const std::string& GetSortedInstanceId(size_t index) const; + const std::string& GetInstanceId(size_t index) const; - unsigned int GetSortedInstanceFramesCount(size_t index) const; + unsigned int GetFramesCount(size_t index) const; void Format(Json::Value& result) const; }; diff -r dba48c162b7b -r 82e88ff003d7 OrthancServer/main.cpp --- a/OrthancServer/main.cpp Tue May 12 07:50:38 2020 +0200 +++ b/OrthancServer/main.cpp Tue May 12 14:58:24 2020 +0200 @@ -89,7 +89,7 @@ toStore.SetJson(dicomJson); std::string id; - context_.Store(id, toStore); + context_.Store(id, toStore, StoreInstanceMode_Default); } } }; @@ -1299,7 +1299,6 @@ HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", "")); - DicomUserConnection::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10)); DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10)); maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10); @@ -1319,7 +1318,7 @@ context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true)); // New option in Orthanc 1.4.2 - context.GetIndex().SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false)); + context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false)); try { diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Tue May 12 14:58:24 2020 +0200 @@ -1761,19 +1761,110 @@ } + class OrthancPlugins::IDicomInstance : public boost::noncopyable + { + public: + virtual ~IDicomInstance() + { + } + + virtual bool CanBeFreed() const = 0; + + virtual const DicomInstanceToStore& GetInstance() const = 0; + }; + + + class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance + { + private: + const DicomInstanceToStore& instance_; + + public: + DicomInstanceFromCallback(const DicomInstanceToStore& instance) : + instance_(instance) + { + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return false; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance + { + private: + std::string buffer_; + DicomInstanceToStore instance_; + + public: + DicomInstanceFromBuffer(const void* buffer, + size_t size) + { + buffer_.assign(reinterpret_cast(buffer), size); + instance_.SetBuffer(buffer_.empty() ? NULL : buffer_.c_str(), buffer_.size()); + instance_.SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance + { + private: + std::unique_ptr parsed_; + DicomInstanceToStore instance_; + + public: + DicomInstanceFromTranscoded(IDicomTranscoder::TranscodedDicom& transcoded) : + parsed_(ParsedDicomFile::AcquireDcmtkObject(transcoded.ReleaseDicom())) + { + instance_.SetParsedDicomFile(*parsed_); + instance_.SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + void OrthancPlugins::SignalStoredInstance(const std::string& instanceId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) { + DicomInstanceFromCallback wrapped(instance); + boost::recursive_mutex::scoped_lock lock(pimpl_->storedCallbackMutex_); for (PImpl::OnStoredCallbacks::const_iterator callback = pimpl_->onStoredCallbacks_.begin(); callback != pimpl_->onStoredCallbacks_.end(); ++callback) { - OrthancPluginErrorCode error = (*callback) - (reinterpret_cast(&instance), - instanceId.c_str()); + OrthancPluginErrorCode error = (*callback) ( + reinterpret_cast(&wrapped), + instanceId.c_str()); if (error != OrthancPluginErrorCode_Success) { @@ -1787,14 +1878,15 @@ bool OrthancPlugins::FilterIncomingInstance(const DicomInstanceToStore& instance, const Json::Value& simplified) { + DicomInstanceFromCallback wrapped(instance); + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); for (PImpl::IncomingDicomInstanceFilters::const_iterator filter = pimpl_->incomingDicomInstanceFilters_.begin(); filter != pimpl_->incomingDicomInstanceFilters_.end(); ++filter) { - int32_t allowed = (*filter) ( - reinterpret_cast(&instance)); + int32_t allowed = (*filter) (reinterpret_cast(&wrapped)); if (allowed == 0) { @@ -2451,14 +2543,19 @@ } - static void AccessDicomInstance(_OrthancPluginService service, - const void* parameters) + void OrthancPlugins::AccessDicomInstance(_OrthancPluginService service, + const void* parameters) { const _OrthancPluginAccessDicomInstance& p = *reinterpret_cast(parameters); + if (p.instance == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + const DicomInstanceToStore& instance = - *reinterpret_cast(p.instance); + reinterpret_cast(p.instance)->GetInstance(); switch (service) { @@ -2523,6 +2620,10 @@ *p.resultInt64 = instance.HasPixelData(); return; + case _OrthancPluginService_GetInstanceFramesCount: // New in Orthanc 1.7.0 + *p.resultInt64 = instance.GetParsedDicomFile().GetFramesCount(); + return; + default: throw OrthancException(ErrorCode_InternalError); } @@ -2606,6 +2707,95 @@ } + void OrthancPlugins::AccessDicomInstance2(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginAccessDicomInstance2& p = + *reinterpret_cast(parameters); + + if (p.instance == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + const DicomInstanceToStore& instance = + reinterpret_cast(p.instance)->GetInstance(); + + switch (service) + { + case _OrthancPluginService_GetInstanceFramesCount: + *p.targetUint32 = instance.GetParsedDicomFile().GetFramesCount(); + return; + + case _OrthancPluginService_GetInstanceRawFrame: + { + if (p.targetBuffer == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + p.targetBuffer->data = NULL; + p.targetBuffer->size = 0; + + MimeType mime; + std::string frame; + instance.GetParsedDicomFile().GetRawFrame(frame, mime, p.frameIndex); + CopyToMemoryBuffer(*p.targetBuffer, frame); + return; + } + + case _OrthancPluginService_GetInstanceDecodedFrame: + { + bool hasDecoderPlugin; + + { + boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + hasDecoderPlugin = !pimpl_->decodeImageCallbacks_.empty(); + } + + std::unique_ptr decoded; + if (p.targetImage == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (hasDecoderPlugin) + { + // TODO - This call could be speeded up the future, if a + // "decoding context" gets introduced in the decoder plugins + + decoded.reset(Decode(instance.GetBufferData(), instance.GetBufferSize(), p.frameIndex)); + } + else + { + decoded.reset(DicomImageDecoder::Decode(instance.GetParsedDicomFile(), p.frameIndex)); + } + + *(p.targetImage) = ReturnImage(decoded); + return; + } + + case _OrthancPluginService_SerializeDicomInstance: + { + if (p.targetBuffer == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + p.targetBuffer->data = NULL; + p.targetBuffer->size = 0; + + std::string serialized; + instance.GetParsedDicomFile().SaveToMemoryBuffer(serialized); + CopyToMemoryBuffer(*p.targetBuffer, serialized); + return; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void OrthancPlugins::UncompressImage(const void* parameters) { const _OrthancPluginUncompressImage& p = *reinterpret_cast(parameters); @@ -2732,10 +2922,12 @@ case OrthancPluginHttpMethod_Post: client.SetMethod(HttpMethod_Post); + client.GetBody().assign(reinterpret_cast(parameters.body), parameters.bodySize); break; case OrthancPluginHttpMethod_Put: client.SetMethod(HttpMethod_Put); + client.GetBody().assign(reinterpret_cast(parameters.body), parameters.bodySize); break; case OrthancPluginHttpMethod_Delete: @@ -3479,6 +3671,13 @@ AccessDicomInstance(service, parameters); return true; + case _OrthancPluginService_GetInstanceFramesCount: + case _OrthancPluginService_GetInstanceRawFrame: + case _OrthancPluginService_GetInstanceDecodedFrame: + case _OrthancPluginService_SerializeDicomInstance: + AccessDicomInstance2(service, parameters); + return true; + case _OrthancPluginService_SetGlobalProperty: { const _OrthancPluginGlobalProperty& p = @@ -4025,6 +4224,78 @@ GetTagName(parameters); return true; + case _OrthancPluginService_CreateDicomInstance: + { + const _OrthancPluginCreateDicomInstance& p = + *reinterpret_cast(parameters); + *(p.target) = reinterpret_cast( + new DicomInstanceFromBuffer(p.buffer, p.size)); + return true; + } + + case _OrthancPluginService_FreeDicomInstance: + { + const _OrthancPluginFreeDicomInstance& p = + *reinterpret_cast(parameters); + + if (p.dicom != NULL) + { + IDicomInstance* obj = reinterpret_cast(p.dicom); + + if (obj->CanBeFreed()) + { + delete obj; + } + else + { + throw OrthancException(ErrorCode_Plugin, "Cannot free a DICOM instance provided to a callback"); + } + } + + return true; + } + + case _OrthancPluginService_TranscodeDicomInstance: + { + const _OrthancPluginCreateDicomInstance& p = + *reinterpret_cast(parameters); + + DicomTransferSyntax transferSyntax; + if (p.transferSyntax == NULL || + !LookupTransferSyntax(transferSyntax, p.transferSyntax)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Unsupported transfer syntax: " + + std::string(p.transferSyntax == NULL ? "(null)" : p.transferSyntax)); + } + else + { + ParsedDicomFile dicom(p.buffer, p.size); + + std::set syntaxes; + syntaxes.insert(transferSyntax); + + std::unique_ptr transcoded; + + { + PImpl::ServerContextLock lock(*pimpl_); + transcoded.reset(lock.GetContext().GetTranscoder().TranscodeToParsed( + dicom.GetDcmtkObject(), p.buffer, p.size, + syntaxes, true /* allow new sop */)); + } + + if (transcoded.get() == NULL) + { + throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode image"); + } + else + { + *(p.target) = reinterpret_cast( + new DicomInstanceFromTranscoded(*transcoded)); + return true; + } + } + } + default: return false; } @@ -4791,4 +5062,18 @@ return NULL; } + + + bool OrthancPlugins::Transcode(std::string& target, + DicomTransferSyntax& sourceSyntax /* out */, + DicomTransferSyntax& targetSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + // TODO + return false; + } } diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.h Tue May 12 14:58:24 2020 +0200 @@ -56,6 +56,7 @@ #include "../../Core/DicomNetworking/IFindRequestHandlerFactory.h" #include "../../Core/DicomNetworking/IMoveRequestHandlerFactory.h" #include "../../Core/DicomNetworking/IWorklistRequestHandlerFactory.h" +#include "../../Core/DicomParsing/MemoryBufferTranscoder.h" #include "../../Core/FileStorage/IStorageArea.h" #include "../../Core/HttpServer/IHttpHandler.h" #include "../../Core/HttpServer/IIncomingHttpRequestFilter.h" @@ -82,7 +83,8 @@ public IIncomingHttpRequestFilter, public IFindRequestHandlerFactory, public IMoveRequestHandlerFactory, - public IStorageCommitmentFactory + public IStorageCommitmentFactory, + public MemoryBufferTranscoder { private: class PImpl; @@ -94,6 +96,10 @@ class HttpClientChunkedRequest; class HttpClientChunkedAnswer; class HttpServerChunkedReader; + class IDicomInstance; + class DicomInstanceFromCallback; + class DicomInstanceFromBuffer; + class DicomInstanceFromTranscoded; void RegisterRestCallback(const void* parameters, bool lock); @@ -155,6 +161,12 @@ void LookupResource(_OrthancPluginService service, const void* parameters); + void AccessDicomInstance(_OrthancPluginService service, + const void* parameters); + + void AccessDicomInstance2(_OrthancPluginService service, + const void* parameters); + void SendHttpStatusCode(const void* parameters); void SendHttpStatus(const void* parameters); @@ -223,6 +235,17 @@ _OrthancPluginService service, const void* parameters); + protected: + // From "MemoryBufferTranscoder" + virtual bool Transcode(std::string& target, + DicomTransferSyntax& sourceSyntax /* out */, + DicomTransferSyntax& targetSyntax /* out */, + bool& hasSopInstanceUidChanged /* out */, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + public: OrthancPlugins(); @@ -250,7 +273,7 @@ virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE; virtual void SignalStoredInstance(const std::string& instanceId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) ORTHANC_OVERRIDE; virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance, diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Tue May 12 14:58:24 2020 +0200 @@ -124,8 +124,8 @@ #endif #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 -#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 6 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 1 +#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 7 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -503,7 +503,14 @@ _OrthancPluginService_GetInstanceOrigin = 4007, _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008, _OrthancPluginService_HasInstancePixelData = 4009, - + _OrthancPluginService_CreateDicomInstance = 4010, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_FreeDicomInstance = 4011, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceFramesCount = 4012, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceRawFrame = 4013, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDecodedFrame = 4014, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_TranscodeDicomInstance = 4015, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_SerializeDicomInstance = 4016, /* New in Orthanc 1.7.0 */ + /* Services for plugins implementing a database back-end */ _OrthancPluginService_RegisterDatabaseBackend = 5000, _OrthancPluginService_DatabaseAnswer = 5001, @@ -7539,6 +7546,164 @@ } + + + + + typedef struct + { + OrthancPluginDicomInstance** target; + const void* buffer; + uint32_t size; + const char* transferSyntax; + } _OrthancPluginCreateDicomInstance; + + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance( + OrthancPluginContext* context, + const void* buffer, + uint32_t size) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginCreateDicomInstance params; + params.target = ⌖ + params.buffer = buffer; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_CreateDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + typedef struct + { + OrthancPluginDicomInstance* dicom; + } _OrthancPluginFreeDicomInstance; + + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeDicomInstance( + OrthancPluginContext* context, + OrthancPluginDicomInstance* dicom) + { + _OrthancPluginFreeDicomInstance params; + params.dicom = dicom; + + context->InvokeService(context, _OrthancPluginService_FreeDicomInstance, ¶ms); + } + + + typedef struct + { + uint32_t* targetUint32; + OrthancPluginMemoryBuffer* targetBuffer; + OrthancPluginImage** targetImage; + const OrthancPluginDicomInstance* instance; + uint32_t frameIndex; + } _OrthancPluginAccessDicomInstance2; + + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + uint32_t count; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetUint32 = &count; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceFramesCount, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginDicomInstance* instance, + uint32_t frameIndex) + { + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetBuffer = target; + params.instance = instance; + params.frameIndex = frameIndex; + + return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, ¶ms); + } + + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + uint32_t frameIndex) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetImage = ⌖ + params.instance = instance; + params.frameIndex = frameIndex; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDecodedFrame, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance( + OrthancPluginContext* context, + const void* buffer, + uint32_t size, + const char* transferSyntax) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginCreateDicomInstance params; + params.target = ⌖ + params.buffer = buffer; + params.size = size; + params.transferSyntax = transferSyntax; + + if (context->InvokeService(context, _OrthancPluginService_TranscodeDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginDicomInstance* instance) + { + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetBuffer = target; + params.instance = instance; + + return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, ¶ms); + } + #ifdef __cplusplus } #endif diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Tue May 12 14:58:24 2020 +0200 @@ -40,6 +40,11 @@ #include +#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0) +static const OrthancPluginErrorCode OrthancPluginErrorCode_NullPointer = OrthancPluginErrorCode_Plugin; +#endif + + namespace OrthancPlugins { static OrthancPluginContext* globalContext_ = NULL; @@ -1045,7 +1050,7 @@ } - const void* OrthancImage::GetBuffer() const + void* OrthancImage::GetBuffer() const { CheckImageAvailable(); return OrthancPluginGetImageBuffer(GetGlobalContext(), image_); @@ -1094,6 +1099,14 @@ } + OrthancPluginImage* OrthancImage::Release() + { + CheckImageAvailable(); + OrthancPluginImage* tmp = image_; + image_ = NULL; + return tmp; + } + #if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) : @@ -3177,4 +3190,182 @@ delete reinterpret_cast(rawHandler); } #endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + DicomInstance::DicomInstance(const OrthancPluginDicomInstance* instance) : + toFree_(false), + instance_(instance) + { + } +#else + DicomInstance::DicomInstance(OrthancPluginDicomInstance* instance) : + toFree_(false), + instance_(instance) + { + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance::DicomInstance(const void* buffer, + size_t size) : + toFree_(true), + instance_(OrthancPluginCreateDicomInstance(GetGlobalContext(), buffer, size)) + { + if (instance_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer); + } + } +#endif + + + DicomInstance::~DicomInstance() + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + if (toFree_ && + instance_ != NULL) + { + OrthancPluginFreeDicomInstance( + GetGlobalContext(), const_cast(instance_)); + } +#endif + } + + + std::string DicomInstance::GetRemoteAet() const + { + const char* s = OrthancPluginGetInstanceRemoteAet(GetGlobalContext(), instance_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return std::string(s); + } + } + + + void DicomInstance::GetJson(Json::Value& target) const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceJson(GetGlobalContext(), instance_)); + s.ToJson(target); + } + + + void DicomInstance::GetSimplifiedJson(Json::Value& target) const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceSimplifiedJson(GetGlobalContext(), instance_)); + s.ToJson(target); + } + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + std::string DicomInstance::GetTransferSyntaxUid() const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceTransferSyntaxUid(GetGlobalContext(), instance_)); + + std::string result; + s.ToString(result); + return result; + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + bool DicomInstance::HasPixelData() const + { + int32_t result = OrthancPluginHasInstancePixelData(GetGlobalContext(), instance_); + if (result < 0) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return (result != 0); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void DicomInstance::GetRawFrame(std::string& target, + unsigned int frameIndex) const + { + MemoryBuffer buffer; + OrthancPluginErrorCode code = OrthancPluginGetInstanceRawFrame( + GetGlobalContext(), *buffer, instance_, frameIndex); + + if (code == OrthancPluginErrorCode_Success) + { + buffer.ToString(target); + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + OrthancImage* DicomInstance::GetDecodedFrame(unsigned int frameIndex) const + { + OrthancPluginImage* image = OrthancPluginGetInstanceDecodedFrame( + GetGlobalContext(), instance_, frameIndex); + + if (image == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return new OrthancImage(image); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void DicomInstance::Serialize(std::string& target) const + { + MemoryBuffer buffer; + OrthancPluginErrorCode code = OrthancPluginSerializeDicomInstance( + GetGlobalContext(), *buffer, instance_); + + if (code == OrthancPluginErrorCode_Success) + { + buffer.ToString(target); + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance* DicomInstance::Transcode(const void* buffer, + size_t size, + const std::string& transferSyntax) + { + OrthancPluginDicomInstance* instance = OrthancPluginTranscodeDicomInstance( + GetGlobalContext(), buffer, size, transferSyntax.c_str()); + + if (instance == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return new DicomInstance(instance); + } + } +#endif } diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h Tue May 12 14:58:24 2020 +0200 @@ -47,6 +47,13 @@ +/** + * The definition of ORTHANC_PLUGINS_VERSION_IS_ABOVE below is for + * backward compatibility with Orthanc SDK <= 1.3.0. + * + * $ hg diff -r Orthanc-1.3.0:Orthanc-1.3.1 ../../../Plugins/Include/orthanc/OrthancCPlugin.h + * + **/ #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) #define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ @@ -379,8 +386,7 @@ uint32_t width, uint32_t height, uint32_t pitch, - void* buffer - ); + void* buffer); ~OrthancImage() { @@ -405,7 +411,7 @@ unsigned int GetPitch() const; - const void* GetBuffer() const; + void* GetBuffer() const; const OrthancPluginImage* GetObject() const { @@ -421,6 +427,10 @@ void AnswerJpegImage(OrthancPluginRestOutput* output, uint8_t quality) const; + + void* GetWriteableBuffer(); + + OrthancPluginImage* Release(); }; @@ -1128,4 +1138,86 @@ static void Destructor(void* rawHandler); }; #endif + + + class DicomInstance : public boost::noncopyable + { + private: + bool toFree_; + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + const OrthancPluginDicomInstance* instance_; +#else + OrthancPluginDicomInstance* instance_; +#endif + + public: +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + DicomInstance(const OrthancPluginDicomInstance* instance); +#else + DicomInstance(OrthancPluginDicomInstance* instance); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance(const void* buffer, + size_t size); +#endif + + ~DicomInstance(); + + std::string GetRemoteAet() const; + + const void* GetBuffer() const + { + return OrthancPluginGetInstanceData(GetGlobalContext(), instance_); + } + + size_t GetSize() const + { + return OrthancPluginGetInstanceSize(GetGlobalContext(), instance_); + } + + void GetJson(Json::Value& target) const; + + void GetSimplifiedJson(Json::Value& target) const; + + OrthancPluginInstanceOrigin GetOrigin() const + { + return OrthancPluginGetInstanceOrigin(GetGlobalContext(), instance_); + } + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + std::string GetTransferSyntaxUid() const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + bool HasPixelData() const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + unsigned int GetFramesCount() const + { + return OrthancPluginGetInstanceFramesCount(GetGlobalContext(), instance_); + } +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void GetRawFrame(std::string& target, + unsigned int frameIndex) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + OrthancImage* GetDecodedFrame(unsigned int frameIndex) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void Serialize(std::string& target) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + static DicomInstance* Transcode(const void* buffer, + size_t size, + const std::string& transferSyntax); +#endif + }; } diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/CMakeLists.txt --- a/Plugins/Samples/GdcmDecoder/CMakeLists.txt Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/CMakeLists.txt Tue May 12 14:58:24 2020 +0200 @@ -1,32 +1,83 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2020 Osimis S.A., Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero 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 +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + cmake_minimum_required(VERSION 2.8) project(GdcmDecoder) -SET(GDCM_DECODER_VERSION "0.0" CACHE STRING "Version of the plugin") -SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") -SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") +SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin") + + +# Parameters of the build +set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") +set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)") +set(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) -include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake) -include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) +# Advanced parameters to fine-tune linking against system libraries +set(USE_SYSTEM_GDCM ON CACHE BOOL "Use the system version of Grassroot DICOM (GDCM)") +set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK") + +# Setup the Orthanc framework +set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../..) + +set(ORTHANC_FRAMEWORK_PLUGIN ON) +include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkParameters.cmake) + +set(ENABLE_LOCALE OFF CACHE INTERNAL "") # Disable support for locales (notably in Boost) +set(ENABLE_MODULE_IMAGES OFF CACHE INTERNAL "") +set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "") -find_package(GDCM REQUIRED) -if (GDCM_FOUND) - include(${GDCM_USE_FILE}) - set(GDCM_LIBRARIES gdcmCommon gdcmMSFF) -else(GDCM_FOUND) - message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?") -endif(GDCM_FOUND) +include(${ORTHANC_ROOT}/Resources/CMake/OrthancFrameworkConfiguration.cmake) + +include(GdcmConfiguration.cmake) + -add_definitions(-DGDCM_DECODER_VERSION="${GDCM_DECODER_VERSION}") +# Check that the Orthanc SDK headers are available +if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK) + #include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-0.9.5) + include_directories(${CMAKE_SOURCE_DIR}/../../Include) # TODO => SYNC 0.9.5 +else () + CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H) + if (NOT HAVE_ORTHANC_H) + message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK") + endif() +endif() + + +include_directories(${ORTHANC_ROOT}) + +add_definitions( + -DPLUGIN_VERSION="${PLUGIN_VERSION}" + -DHAS_ORTHANC_EXCEPTION=1 + -DORTHANC_ENABLE_LOGGING_PLUGIN=1 + ) add_library(GdcmDecoder SHARED - ${BOOST_SOURCES} GdcmDecoderCache.cpp GdcmImageDecoder.cpp - OrthancImageWrapper.cpp Plugin.cpp + ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp + ${ORTHANC_CORE_SOURCES} ) target_link_libraries(GdcmDecoder ${GDCM_LIBRARIES}) + +if (STATIC_BUILD OR NOT USE_SYSTEM_GDCM) + add_dependencies(GdcmDecoder GDCM) +endif() diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/GdcmConfiguration.cmake --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/GdcmConfiguration.cmake Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,141 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2020 Osimis S.A., Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero 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 +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +if (STATIC_BUILD OR NOT USE_SYSTEM_GDCM) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR + ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD") + # If using gcc, build GDCM with the "-fPIC" argument to allow its + # embedding into the shared library containing the Orthanc plugin + set(AdditionalCFlags "-fPIC") + set(AdditionalCxxFlags ${AdditionalCFlags}) + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND + CMAKE_COMPILER_IS_GNUCXX) + # Prevents error: "jump to label ‘err’ crosses initialization" of some variable + # within "Source/Common/gdcmCAPICryptographicMessageSyntax.cxx" if using MinGW + set(AdditionalCxxFlags "-fpermissive") + elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + # This definition is necessary to compile + # "Source/MediaStorageAndFileFormat/gdcmFileStreamer.cxx" + set(AdditionalCFlags "-Doff64_t=off_t") + set(AdditionalCxxFlags ${AdditionalCFlags}) + endif() + + set(Flags + "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} ${AdditionalCFlags}" + "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} ${AdditionalCxxFlags}" + -DCMAKE_C_FLAGS_DEBUG=${CMAKE_C_FLAGS_DEBUG} + -DCMAKE_CXX_FLAGS_DEBUG=${CMAKE_CXX_FLAGS_DEBUG} + -DCMAKE_C_FLAGS_RELEASE=${CMAKE_C_FLAGS_RELEASE} + -DCMAKE_CXX_FLAGS_RELEASE=${CMAKE_CXX_FLAGS_RELEASE} + -DCMAKE_C_FLAGS_MINSIZEREL=${CMAKE_C_FLAGS_MINSIZEREL} + -DCMAKE_CXX_FLAGS_MINSIZEREL=${CMAKE_CXX_FLAGS_MINSIZEREL} + -DCMAKE_C_FLAGS_RELWITHDEBINFO=${CMAKE_C_FLAGS_RELWITHDEBINFO} + -DCMAKE_CXX_FLAGS_RELWITHDEBINFO=${CMAKE_CXX_FLAGS_RELWITHDEBINFO} + ) + + if (CMAKE_TOOLCHAIN_FILE) + # Take absolute path to the toolchain + get_filename_component(TMP ${CMAKE_TOOLCHAIN_FILE} REALPATH BASE ${CMAKE_SOURCE_DIR}) + list(APPEND Flags -DCMAKE_TOOLCHAIN_FILE=${TMP}) + endif() + + # Don't build manpages (since gdcm 2.8.4) + list(APPEND Flags -DGDCM_BUILD_DOCBOOK_MANPAGES=OFF) + + if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase") + # Trick to disable the compilation of socket++ by gdcm, which is + # incompatible with LSB, but fortunately only required for DICOM + # Networking + list(APPEND Flags -DGDCM_USE_SYSTEM_SOCKETXX=ON) + + # Detect the number of CPU cores to run "make" with as much + # parallelism as possible + include(ProcessorCount) + ProcessorCount(N) + if (NOT N EQUAL 0) + set(MAKE_PARALLEL -j${N}) + endif() + + # For Linux Standard Base, avoid building incompatible target gdcmMEXD (*) + set(BUILD_COMMAND BUILD_COMMAND + ${CMAKE_MAKE_PROGRAM} ${MAKE_PARALLEL} + gdcmMSFF gdcmcharls gdcmDICT gdcmDSED gdcmIOD gdcmjpeg8 + gdcmjpeg12 gdcmjpeg16 gdcmopenjp2 gdcmzlib gdcmCommon gdcmexpat) + endif() + + include(ExternalProject) + externalproject_add(GDCM + URL "http://orthanc.osimis.io/ThirdPartyDownloads/gdcm-3.0.4.tar.gz" + URL_MD5 "f12dbded708356d5fa0b5ed37ccdb66e" + TIMEOUT 60 + CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} ${Flags} + ${BUILD_COMMAND} # Customize "make", only for Linux Standard Base (*) + INSTALL_COMMAND "" # Skip the install step + ) + + if(MSVC) + set(Suffix ".lib") + set(Prefix "") + else() + set(Suffix ".a") + list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix) + endif() + + set(GDCM_LIBRARIES + # WARNING: The order of the libraries below *is* important! + ${Prefix}gdcmMSFF${Suffix} + ${Prefix}gdcmcharls${Suffix} + ${Prefix}gdcmDICT${Suffix} + ${Prefix}gdcmDSED${Suffix} + ${Prefix}gdcmIOD${Suffix} + ${Prefix}gdcmjpeg8${Suffix} + ${Prefix}gdcmjpeg12${Suffix} + ${Prefix}gdcmjpeg16${Suffix} + ${Prefix}gdcmopenjp2${Suffix} + ${Prefix}gdcmzlib${Suffix} + ${Prefix}gdcmCommon${Suffix} + ${Prefix}gdcmexpat${Suffix} + + #${Prefix}socketxx${Suffix} + #${Prefix}gdcmMEXD${Suffix} # DICOM Networking, unneeded by Orthanc plugins + #${Prefix}gdcmgetopt${Suffix} + #${Prefix}gdcmuuid${Suffix} + ) + + ExternalProject_Get_Property(GDCM binary_dir) + include_directories(${binary_dir}/Source/Common) + link_directories(${binary_dir}/bin) + + ExternalProject_Get_Property(GDCM source_dir) + include_directories( + ${source_dir}/Source/Common + ${source_dir}/Source/MediaStorageAndFileFormat + ${source_dir}/Source/DataStructureAndEncodingDefinition + ) + +else() + find_package(GDCM REQUIRED) + if (GDCM_FOUND) + include(${GDCM_USE_FILE}) + set(GDCM_LIBRARIES gdcmCommon gdcmMSFF) + else(GDCM_FOUND) + message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?") + endif(GDCM_FOUND) +endif() diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp --- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Tue May 12 14:58:24 2020 +0200 @@ -22,17 +22,15 @@ #include "GdcmDecoderCache.h" #include "../../../Core/Compatibility.h" -#include "OrthancImageWrapper.h" namespace OrthancPlugins { - std::string GdcmDecoderCache::ComputeMd5(OrthancPluginContext* context, - const void* dicom, + std::string GdcmDecoderCache::ComputeMd5(const void* dicom, size_t size) { std::string result; - char* md5 = OrthancPluginComputeMd5(context, dicom, size); + char* md5 = OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), dicom, size); if (md5 == NULL) { @@ -49,7 +47,7 @@ { } - OrthancPluginFreeString(context, md5); + OrthancPluginFreeString(OrthancPlugins::GetGlobalContext(), md5); if (!ok) { @@ -62,12 +60,11 @@ } - OrthancImageWrapper* GdcmDecoderCache::Decode(OrthancPluginContext* context, - const void* dicom, - const uint32_t size, - uint32_t frameIndex) + OrthancImage* GdcmDecoderCache::Decode(const void* dicom, + const uint32_t size, + uint32_t frameIndex) { - std::string md5 = ComputeMd5(context, dicom, size); + std::string md5 = ComputeMd5(dicom, size); // First check whether the previously decoded image is the same // as this one @@ -79,13 +76,13 @@ md5_ == md5) { // This is the same image: Reuse the previous decoding - return new OrthancImageWrapper(context, decoder_->Decode(context, frameIndex)); + return new OrthancImage(decoder_->Decode(frameIndex)); } } // This is not the same image std::unique_ptr decoder(new GdcmImageDecoder(dicom, size)); - std::unique_ptr image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex))); + std::unique_ptr image(new OrthancImage(decoder->Decode(frameIndex))); { // Cache the newly created decoder for further use diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h --- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Tue May 12 14:58:24 2020 +0200 @@ -23,7 +23,7 @@ #include "../../../Core/Compatibility.h" #include "GdcmImageDecoder.h" -#include "OrthancImageWrapper.h" +#include "../Common/OrthancPluginCppWrapper.h" #include @@ -38,8 +38,7 @@ size_t size_; std::string md5_; - static std::string ComputeMd5(OrthancPluginContext* context, - const void* dicom, + static std::string ComputeMd5(const void* dicom, size_t size); public: @@ -47,9 +46,8 @@ { } - OrthancImageWrapper* Decode(OrthancPluginContext* context, - const void* dicom, - const uint32_t size, - uint32_t frameIndex); + OrthancImage* Decode(const void* dicom, + const uint32_t size, + uint32_t frameIndex); }; } diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp --- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Tue May 12 14:58:24 2020 +0200 @@ -22,7 +22,6 @@ #include "GdcmImageDecoder.h" #include "../../../Core/Compatibility.h" -#include "OrthancImageWrapper.h" #include #include @@ -298,7 +297,7 @@ } - static void FixPhotometricInterpretation(OrthancImageWrapper& image, + static void FixPhotometricInterpretation(OrthancImage& image, gdcm::PhotometricInterpretation interpretation) { switch (interpretation) @@ -317,7 +316,7 @@ uint32_t pitch = image.GetPitch(); uint8_t* buffer = reinterpret_cast(image.GetBuffer()); - if (image.GetFormat() != OrthancPluginPixelFormat_RGB24 || + if (image.GetPixelFormat() != OrthancPluginPixelFormat_RGB24 || pitch < 3 * width) { throw std::runtime_error("Internal error"); @@ -346,8 +345,7 @@ } - OrthancPluginImage* GdcmImageDecoder::Decode(OrthancPluginContext* context, - unsigned int frameIndex) const + OrthancPluginImage* GdcmImageDecoder::Decode(unsigned int frameIndex) const { unsigned int frames = GetFramesCount(); unsigned int width = GetWidth(); @@ -361,7 +359,7 @@ } std::string& decoded = pimpl_->decoded_; - OrthancImageWrapper target(context, format, width, height); + OrthancImage target(format, width, height); if (width == 0 || height == 0) @@ -391,8 +389,9 @@ size_t targetPitch = target.GetPitch(); size_t sourcePitch = width * bpp; - const char* a = &decoded[sourcePitch * height * frameIndex]; - char* b = target.GetBuffer(); + const uint8_t* a = (reinterpret_cast(decoded.c_str()) + + sourcePitch * height * frameIndex); + uint8_t* b = reinterpret_cast(target.GetBuffer()); for (uint32_t y = 0; y < height; y++) { diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h --- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h Tue May 12 14:58:24 2020 +0200 @@ -21,23 +21,13 @@ #pragma once -#include +#include "../Common/OrthancPluginCppWrapper.h" + #include #include #include -// This is for compatibility with Orthanc SDK <= 1.3.0 -#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) -#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ - (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ - (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ - ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision)))) -#endif - - namespace OrthancPlugins { class GdcmImageDecoder : public boost::noncopyable @@ -60,7 +50,6 @@ static size_t GetBytesPerPixel(OrthancPluginPixelFormat format); - OrthancPluginImage* Decode(OrthancPluginContext* context, - unsigned int frameIndex) const; + OrthancPluginImage* Decode(unsigned int frameIndex) const; }; } diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/OrthancImageWrapper.cpp --- a/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "OrthancImageWrapper.h" - -#include - -namespace OrthancPlugins -{ - OrthancImageWrapper::OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginPixelFormat format, - uint32_t width, - uint32_t height) : - context_(context) - { - image_ = OrthancPluginCreateImage(context_, format, width, height); - if (image_ == NULL) - { - throw std::runtime_error("Cannot create an image"); - } - } - - - OrthancImageWrapper::OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginImage* image) : - context_(context), - image_(image) - { - if (image_ == NULL) - { - throw std::runtime_error("Invalid image returned by the core of Orthanc"); - } - } - - - - OrthancImageWrapper::~OrthancImageWrapper() - { - if (image_ != NULL) - { - OrthancPluginFreeImage(context_, image_); - } - } - - - OrthancPluginImage* OrthancImageWrapper::Release() - { - OrthancPluginImage* tmp = image_; - image_ = NULL; - return tmp; - } - - - uint32_t OrthancImageWrapper::GetWidth() - { - return OrthancPluginGetImageWidth(context_, image_); - } - - - uint32_t OrthancImageWrapper::GetHeight() - { - return OrthancPluginGetImageHeight(context_, image_); - } - - - uint32_t OrthancImageWrapper::GetPitch() - { - return OrthancPluginGetImagePitch(context_, image_); - } - - - OrthancPluginPixelFormat OrthancImageWrapper::GetFormat() - { - return OrthancPluginGetImagePixelFormat(context_, image_); - } - - - char* OrthancImageWrapper::GetBuffer() - { - return reinterpret_cast(OrthancPluginGetImageBuffer(context_, image_)); - } -} diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/OrthancImageWrapper.h --- a/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include - -#include "GdcmImageDecoder.h" - -namespace OrthancPlugins -{ - class OrthancImageWrapper - { - private: - OrthancPluginContext* context_; - OrthancPluginImage* image_; - - public: - OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginPixelFormat format, - uint32_t width, - uint32_t height); - - OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginImage* image); // Takes ownership - - ~OrthancImageWrapper(); - - OrthancPluginContext* GetContext() - { - return context_; - } - - OrthancPluginImage* Release(); - - uint32_t GetWidth(); - - uint32_t GetHeight(); - - uint32_t GetPitch(); - - OrthancPluginPixelFormat GetFormat(); - - char* GetBuffer(); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Plugins/Samples/GdcmDecoder/Plugin.cpp --- a/Plugins/Samples/GdcmDecoder/Plugin.cpp Tue May 12 07:50:38 2020 +0200 +++ b/Plugins/Samples/GdcmDecoder/Plugin.cpp Tue May 12 14:58:24 2020 +0200 @@ -20,13 +20,81 @@ #include "../../../Core/Compatibility.h" +#include "../../../Core/DicomFormat/DicomMap.h" +#include "../../../Core/Toolbox.h" #include "GdcmDecoderCache.h" -#include "OrthancImageWrapper.h" + +static OrthancPlugins::GdcmDecoderCache cache_; +static bool restrictTransferSyntaxes_ = false; +static std::set enabledTransferSyntaxes_; + + +static bool ExtractTransferSyntax(std::string& transferSyntax, + const void* dicom, + const uint32_t size) +{ + Orthanc::DicomMap header; + if (!Orthanc::DicomMap::ParseDicomMetaInformation(header, reinterpret_cast(dicom), size)) + { + return false; + } + + const Orthanc::DicomValue* tag = header.TestAndGetValue(0x0002, 0x0010); + if (tag == NULL || + tag->IsNull() || + tag->IsBinary()) + { + return false; + } + else + { + // Stripping spaces should not be required, as this is a UI value + // representation whose stripping is supported by the Orthanc + // core, but let's be careful... + transferSyntax = Orthanc::Toolbox::StripSpaces(tag->GetContent()); + return true; + } +} + -#include +static bool IsTransferSyntaxEnabled(const void* dicom, + const uint32_t size) +{ + std::string formattedSize; + + { + char tmp[16]; + sprintf(tmp, "%0.1fMB", static_cast(size) / (1024.0f * 1024.0f)); + formattedSize.assign(tmp); + } + + if (!restrictTransferSyntaxes_) + { + LOG(INFO) << "Decoding one DICOM instance of " << formattedSize << " using GDCM"; + return true; + } -static OrthancPluginContext* context_ = NULL; -static OrthancPlugins::GdcmDecoderCache cache_; + std::string transferSyntax; + if (!ExtractTransferSyntax(transferSyntax, dicom, size)) + { + LOG(INFO) << "Cannot extract the transfer syntax of this instance of " + << formattedSize << ", will use GDCM to decode it"; + return true; + } + else if (enabledTransferSyntaxes_.find(transferSyntax) != enabledTransferSyntaxes_.end()) + { + // Decoding for this transfer syntax is enabled + LOG(INFO) << "Using GDCM to decode this instance of " << formattedSize + << " with transfer syntax " << transferSyntax; + return true; + } + else + { + LOG(INFO) << "Won't use GDCM to decode this instance of " << formattedSize + << ", as its transfer syntax " << transferSyntax << " is disabled"; + return false; + } +} static OrthancPluginErrorCode DecodeImageCallback(OrthancPluginImage** target, @@ -36,62 +104,135 @@ { try { - std::unique_ptr image; + if (!IsTransferSyntaxEnabled(dicom, size)) + { + *target = NULL; + return OrthancPluginErrorCode_Success; + } + + std::unique_ptr image; #if 0 // Do not use the cache OrthancPlugins::GdcmImageDecoder decoder(dicom, size); - image.reset(new OrthancPlugins::OrthancImageWrapper(context_, decoder.Decode(context_, frameIndex))); + image.reset(new OrthancPlugins::OrthancImage(decoder.Decode(frameIndex))); #else - image.reset(cache_.Decode(context_, dicom, size, frameIndex)); + image.reset(cache_.Decode(dicom, size, frameIndex)); #endif *target = image->Release(); return OrthancPluginErrorCode_Success; } + catch (Orthanc::OrthancException& e) + { + *target = NULL; + + LOG(WARNING) << "Cannot decode image using GDCM: " << e.What(); + return OrthancPluginErrorCode_Plugin; + } catch (std::runtime_error& e) { *target = NULL; - std::string s = "Cannot decode image using GDCM: " + std::string(e.what()); - OrthancPluginLogInfo(context_, s.c_str()); + LOG(WARNING) << "Cannot decode image using GDCM: " << e.what(); + return OrthancPluginErrorCode_Plugin; + } + catch (...) + { + *target = NULL; + + LOG(WARNING) << "Native exception while decoding image using GDCM"; return OrthancPluginErrorCode_Plugin; } } +/** + * We force the redefinition of the "ORTHANC_PLUGINS_API" macro, that + * was left empty with gcc until Orthanc SDK 1.5.7 (no "default" + * visibility). This causes the version script, if run from "Holy + * Build Box", to make private the 4 global functions of the plugin. + **/ + +#undef ORTHANC_PLUGINS_API + +#ifdef WIN32 +# define ORTHANC_PLUGINS_API __declspec(dllexport) +#elif __GNUC__ >= 4 +# define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default"))) +#else +# define ORTHANC_PLUGINS_API +#endif + + extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) { - context_ = context; - OrthancPluginLogWarning(context_, "Initializing the advanced decoder of medical images using GDCM"); + static const char* const KEY_GDCM = "Gdcm"; + static const char* const KEY_ENABLE_GDCM = "EnableGdcm"; + static const char* const KEY_RESTRICT_TRANSFER_SYNTAXES = "RestrictTransferSyntaxes"; + + OrthancPlugins::SetGlobalContext(context); + LOG(INFO) << "Initializing the advanced decoder of medical images using GDCM"; /* Check the version of the Orthanc core */ - if (OrthancPluginCheckVersion(context_) == 0) + if (OrthancPluginCheckVersion(context) == 0) { char info[1024]; sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", - context_->orthancVersion, + context->orthancVersion, ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); - OrthancPluginLogError(context_, info); + OrthancPluginLogError(context, info); return -1; } - OrthancPluginSetDescription(context_, "Advanced decoder of medical images using GDCM."); - OrthancPluginRegisterDecodeImageCallback(context_, DecodeImageCallback); + OrthancPluginSetDescription(context, "Advanced decoder of medical images using GDCM."); + + OrthancPlugins::OrthancConfiguration global; + + bool enabled = true; + + if (global.IsSection(KEY_GDCM)) + { + OrthancPlugins::OrthancConfiguration config; + global.GetSection(config, KEY_GDCM); + + enabled = config.GetBooleanValue(KEY_ENABLE_GDCM, true); + if (config.LookupSetOfStrings(enabledTransferSyntaxes_, KEY_RESTRICT_TRANSFER_SYNTAXES, false)) + { + restrictTransferSyntaxes_ = true; + + for (std::set::const_iterator it = enabledTransferSyntaxes_.begin(); + it != enabledTransferSyntaxes_.end(); ++it) + { + LOG(WARNING) << "Orthanc will use GDCM to decode transfer syntax: " << *it; + } + } + } + + if (enabled) + { + OrthancPluginRegisterDecodeImageCallback(context, DecodeImageCallback); + } + else + { + LOG(WARNING) << "The advanced decoder of medical images using GDCM is disabled"; + } + return 0; } ORTHANC_PLUGINS_API void OrthancPluginFinalize() { + LOG(INFO) << "Finalizing the advanced decoder of medical images using GDCM"; } @@ -103,6 +244,6 @@ ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() { - return GDCM_DECODER_VERSION; + return PLUGIN_VERSION; } } diff -r dba48c162b7b -r 82e88ff003d7 Resources/CMake/DcmtkConfiguration.cmake --- a/Resources/CMake/DcmtkConfiguration.cmake Tue May 12 07:50:38 2020 +0200 +++ b/Resources/CMake/DcmtkConfiguration.cmake Tue May 12 14:58:24 2020 +0200 @@ -36,6 +36,14 @@ ) endif() + if (ENABLE_DCMTK_TRANSCODING) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimgle/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimage/libsrc DCMTK_SOURCES) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmimage/include + ) + endif() + if (ENABLE_DCMTK_JPEG) AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc DCMTK_SOURCES) AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8 DCMTK_SOURCES) @@ -56,17 +64,21 @@ ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8/jaricom.c ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12/jaricom.c ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg24/jaricom.c + ) - # Disable support for encoding JPEG (modification in Orthanc 1.0.1) - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djcodece.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsv1.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencbas.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencpro.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djenclol.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencode.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencext.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsps.cc - ) + if (NOT ENABLE_DCMTK_TRANSCODING) + list(REMOVE_ITEM DCMTK_SOURCES + # Disable support for encoding JPEG (modification in Orthanc 1.0.1) + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djcodece.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsv1.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencbas.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencpro.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djenclol.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencode.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencext.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsps.cc + ) + endif() endif() @@ -78,15 +90,18 @@ ${DCMTK_SOURCES_DIR}/dcmjpls/include ${DCMTK_SOURCES_DIR}/dcmjpls/libcharls ) - list(REMOVE_ITEM DCMTK_SOURCES - ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djcodece.cc - - # Disable support for encoding JPEG-LS (modification in Orthanc 1.0.1) - ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djencode.cc - ) list(APPEND DCMTK_SOURCES ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djrplol.cc ) + + if (NOT ENABLE_DCMTK_TRANSCODING) + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djcodece.cc + + # Disable support for encoding JPEG-LS (modification in Orthanc 1.0.1) + ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djencode.cc + ) + endif() endif() diff -r dba48c162b7b -r 82e88ff003d7 Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake --- a/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake Tue May 12 07:50:38 2020 +0200 +++ b/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake Tue May 12 14:58:24 2020 +0200 @@ -1,6 +1,6 @@ -SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.1.1f) -SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.1.1f.tar.gz") -SET(OPENSSL_MD5 "3f486f2f4435ef14b81814dbbc7b48bb") +SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.1.1g) +SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.1.1g.tar.gz") +SET(OPENSSL_MD5 "76766e98997660138cdaf13a187bd234") if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}") set(FirstRun OFF) @@ -27,7 +27,7 @@ # Apply the patches execute_process( COMMAND ${PATCH_EXECUTABLE} -p0 -N -i - ${ORTHANC_ROOT}/Resources/Patches/openssl-1.1.1f.patch + ${ORTHANC_ROOT}/Resources/Patches/openssl-1.1.1g.patch WORKING_DIRECTORY ${CMAKE_BINARY_DIR} RESULT_VARIABLE Failure ) diff -r dba48c162b7b -r 82e88ff003d7 Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue May 12 07:50:38 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue May 12 14:58:24 2020 +0200 @@ -484,10 +484,9 @@ ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociation.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociationParameters.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomControlUserConnection.cpp - ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp - ${ORTHANC_ROOT}/Core/DicomNetworking/DicomUserConnection.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/CommandDispatcher.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/FindScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/MoveScp.cpp @@ -503,6 +502,11 @@ # New in Orthanc 1.6.0 if (ENABLE_DCMTK_TRANSCODING) add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=1) + list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL + ${ORTHANC_ROOT}/Core/DicomParsing/DcmtkTranscoder.cpp + ${ORTHANC_ROOT}/Core/DicomParsing/IDicomTranscoder.cpp + ${ORTHANC_ROOT}/Core/DicomParsing/MemoryBufferTranscoder.cpp + ) else() add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=0) endif() diff -r dba48c162b7b -r 82e88ff003d7 Resources/CMake/OrthancFrameworkParameters.cmake --- a/Resources/CMake/OrthancFrameworkParameters.cmake Tue May 12 07:50:38 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkParameters.cmake Tue May 12 14:58:24 2020 +0200 @@ -17,7 +17,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "6") +set(ORTHANC_API_VERSION "7") ##################################################################### diff -r dba48c162b7b -r 82e88ff003d7 Resources/Configuration.json --- a/Resources/Configuration.json Tue May 12 07:50:38 2020 +0200 +++ b/Resources/Configuration.json Tue May 12 14:58:24 2020 +0200 @@ -209,9 +209,17 @@ * registered remote SCU modalities. Starting with Orthanc 1.5.0, * it is possible to specify which DICOM commands are allowed, * separately for each remote modality, using the syntax - * below. The "AllowEcho" (resp. "AllowStore") option only has an - * effect respectively if global option "DicomAlwaysAllowEcho" - * (resp. "DicomAlwaysAllowStore") is set to false. + * below. + * + * The "AllowEcho" (resp. "AllowStore") option only has an effect + * respectively if global option "DicomAlwaysAllowEcho" + * (resp. "DicomAlwaysAllowStore") is set to "false". + * + * Starting with Orthanc 1.7.0, "AllowTranscoding" can be used to + * disable the transcoding to uncompressed transfer syntaxes if + * the remote modality doesn't support compressed transfer + * syntaxes. This option only has an effect if global option + * "EnableTranscoding" is set to "true". **/ //"untrusted" : { // "AET" : "ORTHANC", @@ -223,7 +231,8 @@ // "AllowGet" : false, // "AllowMove" : false, // "AllowStore" : true, - // "AllowStorageCommitment" : false // new in 1.6.0 + // "AllowStorageCommitment" : false, // new in 1.6.0 + // "AllowTranscoding" : true // new in 1.7.0 //} }, @@ -533,5 +542,10 @@ // Maximum number of storage commitment reports (i.e. received from // remote modalities) to be kept in memory (new in Orthanc 1.6.0). - "StorageCommitmentReportsSize" : 100 + "StorageCommitmentReportsSize" : 100, + + // Whether Orthanc transcodes DICOM files to an uncompressed + // transfer syntax over the DICOM protocol, if the remote modality + // does not support compressed transfer syntaxes (new in Orthanc 1.7.0). + "TranscodeDicomProtocol" : true } diff -r dba48c162b7b -r 82e88ff003d7 Resources/DicomTransferSyntaxes.json --- a/Resources/DicomTransferSyntaxes.json Tue May 12 07:50:38 2020 +0200 +++ b/Resources/DicomTransferSyntaxes.json Tue May 12 14:58:24 2020 +0200 @@ -274,7 +274,7 @@ { "UID" : "1.2.840.10008.1.2.4.100", - "Name" : "MPEG2 Main Profile at Main Level", + "Name" : "MPEG2 Main Profile / Main Level", "Value" : "MPEG2MainProfileAtMainLevel", "Retired" : false, "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel" @@ -282,7 +282,7 @@ { "UID" : "1.2.840.10008.1.2.4.101", - "Name" : "MPEG2 Main Profile at High Level", + "Name" : "MPEG2 Main Profile / High Level", "Value" : "MPEG2MainProfileAtHighLevel", "Retired" : false, "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel" @@ -290,7 +290,7 @@ { "UID" : "1.2.840.10008.1.2.4.102", - "Name" : "MPEG4 High Profile / Level 4.1", + "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.1", "Value" : "MPEG4HighProfileLevel4_1", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_1", @@ -299,7 +299,7 @@ { "UID" : "1.2.840.10008.1.2.4.103", - "Name" : "MPEG4 BD-compatible High Profile / Level 4.1", + "Name" : "MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1", "Value" : "MPEG4BDcompatibleHighProfileLevel4_1", "Retired" : false, "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1", @@ -308,7 +308,7 @@ { "UID" : "1.2.840.10008.1.2.4.104", - "Name" : "MPEG4 High Profile / Level 4.2 For 2D Video", + "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video", "Value" : "MPEG4HighProfileLevel4_2_For2DVideo", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo", @@ -317,7 +317,7 @@ { "UID" : "1.2.840.10008.1.2.4.105", - "Name" : "MPEG4 High Profile / Level 4.2 For 3D Video", + "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video", "Value" : "MPEG4HighProfileLevel4_2_For3DVideo", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo", @@ -326,7 +326,7 @@ { "UID" : "1.2.840.10008.1.2.4.106", - "Name" : "1.2.840.10008.1.2.4.106", + "Name" : "MPEG4 AVC/H.264 Stereo High Profile / Level 4.2", "Value" : "MPEG4StereoHighProfileLevel4_2", "Retired" : false, "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2", diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/CallSystemCommand.cpp --- a/Resources/Graveyard/OldScheduler/CallSystemCommand.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "CallSystemCommand.h" - -#include "../../Core/Logging.h" -#include "../../Core/Toolbox.h" -#include "../../Core/TemporaryFile.h" - -namespace Orthanc -{ - CallSystemCommand::CallSystemCommand(ServerContext& context, - const std::string& command, - const std::vector& arguments) : - context_(context), - command_(command), - arguments_(arguments) - { - } - - bool CallSystemCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Calling system command " << command_ << " on instance " << *it; - - try - { - std::string dicom; - context_.ReadDicom(dicom, *it); - - TemporaryFile tmp; - tmp.Write(dicom); - - std::vector args = arguments_; - args.push_back(tmp.GetPath()); - - SystemToolbox::ExecuteSystemCommand(command_, args); - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to call system command " << command_ - << " on instance " << *it << " in a Lua script: " << e.What(); - } - } - - return true; - } -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/CallSystemCommand.h --- a/Resources/Graveyard/OldScheduler/CallSystemCommand.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class CallSystemCommand : public IServerCommand - { - private: - ServerContext& context_; - std::string command_; - std::vector arguments_; - - public: - CallSystemCommand(ServerContext& context, - const std::string& command, - const std::vector& arguments); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/DeleteInstanceCommand.cpp --- a/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "DeleteInstanceCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - bool DeleteInstanceCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Deleting instance " << *it; - - try - { - Json::Value tmp; - context_.DeleteResource(tmp, *it, ResourceType_Instance); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to delete instance " << *it << ": " << e.What(); - } - } - - return true; - } -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/DeleteInstanceCommand.h --- a/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class DeleteInstanceCommand : public IServerCommand - { - private: - ServerContext& context_; - - public: - DeleteInstanceCommand(ServerContext& context) : context_(context) - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/IServerCommand.h --- a/Resources/Graveyard/OldScheduler/IServerCommand.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include -#include -#include - -namespace Orthanc -{ - class IServerCommand : public boost::noncopyable - { - public: - typedef std::list ListOfStrings; - - virtual ~IServerCommand() - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) = 0; - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ModifyInstanceCommand.cpp --- a/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ModifyInstanceCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context, - RequestOrigin origin, - DicomModification* modification) : - context_(context), - origin_(origin), - modification_(modification) - { - modification_->SetAllowManualIdentifiers(true); - - if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID)) - { - modification_->SetLevel(ResourceType_Patient); - } - else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) - { - modification_->SetLevel(ResourceType_Study); - } - else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) - { - modification_->SetLevel(ResourceType_Series); - } - else - { - modification_->SetLevel(ResourceType_Instance); - } - - if (origin_ != RequestOrigin_Lua) - { - // TODO If issued from HTTP, "remoteIp" and "username" must be provided - throw OrthancException(ErrorCode_NotImplemented); - } - } - - - ModifyInstanceCommand::~ModifyInstanceCommand() - { - if (modification_) - { - delete modification_; - } - } - - - bool ModifyInstanceCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Modifying resource " << *it; - - try - { - std::auto_ptr modified; - - { - ServerContext::DicomCacheLocker lock(context_, *it); - modified.reset(lock.GetDicom().Clone(true)); - } - - modification_->Apply(*modified); - - DicomInstanceToStore toStore; - assert(origin_ == RequestOrigin_Lua); - toStore.SetLuaOrigin(); - toStore.SetParsedDicomFile(*modified); - // TODO other metadata - toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it); - - std::string modifiedId; - context_.Store(modifiedId, toStore); - - // Only chain with other commands if this command succeeds - outputs.push_back(modifiedId); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to modify instance " << *it << ": " << e.What(); - } - } - - return true; - } -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ModifyInstanceCommand.h --- a/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" -#include "../../Core/DicomParsing/DicomModification.h" - -namespace Orthanc -{ - class ModifyInstanceCommand : public IServerCommand - { - private: - ServerContext& context_; - RequestOrigin origin_; - DicomModification* modification_; - - public: - ModifyInstanceCommand(ServerContext& context, - RequestOrigin origin, - DicomModification* modification); // takes the ownership - - virtual ~ModifyInstanceCommand(); - - const DicomModification& GetModification() const - { - return *modification_; - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.cpp --- a/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,188 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeaders.h" -#include "ReusableDicomUserConnection.h" - -#include "../Logging.h" -#include "../OrthancException.h" - -namespace Orthanc -{ - static boost::posix_time::ptime Now() - { - return boost::posix_time::microsec_clock::local_time(); - } - - void ReusableDicomUserConnection::Open(const std::string& localAet, - const RemoteModalityParameters& remote) - { - if (connection_ != NULL && - connection_->GetLocalApplicationEntityTitle() == localAet && - connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() && - connection_->GetRemoteHost() == remote.GetHost() && - connection_->GetRemotePort() == remote.GetPort() && - connection_->GetRemoteManufacturer() == remote.GetManufacturer()) - { - // The current connection can be reused - LOG(INFO) << "Reusing the previous SCU connection"; - return; - } - - Close(); - - connection_ = new DicomUserConnection(); - connection_->SetLocalApplicationEntityTitle(localAet); - connection_->SetRemoteModality(remote); - connection_->Open(); - } - - void ReusableDicomUserConnection::Close() - { - if (connection_ != NULL) - { - delete connection_; - connection_ = NULL; - } - } - - void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that) - { - for (;;) - { - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); - if (!that->continue_) - { - //LOG(INFO) << "Finishing the thread watching the global SCU connection"; - return; - } - - { - boost::mutex::scoped_lock lock(that->mutex_); - if (that->connection_ != NULL && - Now() >= that->lastUse_ + that->timeBeforeClose_) - { - LOG(INFO) << "Closing the global SCU connection after timeout"; - that->Close(); - } - } - } - } - - - ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that, - const std::string& localAet, - const RemoteModalityParameters& remote) : - ::Orthanc::Locker(that) - { - that.Open(localAet, remote); - connection_ = that.connection_; - } - - - DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection() - { - if (connection_ == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - return *connection_; - } - - ReusableDicomUserConnection::ReusableDicomUserConnection() : - connection_(NULL), - timeBeforeClose_(boost::posix_time::seconds(5)) // By default, close connection after 5 seconds - { - lastUse_ = Now(); - continue_ = true; - closeThread_ = boost::thread(CloseThread, this); - } - - ReusableDicomUserConnection::~ReusableDicomUserConnection() - { - if (continue_) - { - LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!"; - Finalize(); - } - } - - void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms) - { - boost::mutex::scoped_lock lock(mutex_); - - if (ms == 0) - { - ms = 1; - } - - timeBeforeClose_ = boost::posix_time::milliseconds(ms); - } - - void ReusableDicomUserConnection::Lock() - { - mutex_.lock(); - } - - void ReusableDicomUserConnection::Unlock() - { - if (connection_ != NULL && - connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp) - { - // "storescp" from DCMTK has problems when reusing a - // connection. Always close. - Close(); - } - - lastUse_ = Now(); - mutex_.unlock(); - } - - - void ReusableDicomUserConnection::Finalize() - { - if (continue_) - { - continue_ = false; - - if (closeThread_.joinable()) - { - closeThread_.join(); - } - - Close(); - } - } -} - diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.h --- a/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "DicomUserConnection.h" -#include "../../Core/MultiThreading/Locker.h" - -#include -#include - -namespace Orthanc -{ - class ReusableDicomUserConnection : public ILockable - { - private: - boost::mutex mutex_; - DicomUserConnection* connection_; - bool continue_; - boost::posix_time::time_duration timeBeforeClose_; - boost::posix_time::ptime lastUse_; - boost::thread closeThread_; - - void Open(const std::string& localAet, - const RemoteModalityParameters& remote); - - void Close(); - - static void CloseThread(ReusableDicomUserConnection* that); - - protected: - virtual void Lock(); - - virtual void Unlock(); - - public: - class Locker : public ::Orthanc::Locker - { - private: - DicomUserConnection* connection_; - - public: - Locker(ReusableDicomUserConnection& that, - const std::string& localAet, - const RemoteModalityParameters& remote); - - DicomUserConnection& GetConnection(); - }; - - ReusableDicomUserConnection(); - - virtual ~ReusableDicomUserConnection(); - - void SetMillisecondsBeforeClose(uint64_t ms); - - void Finalize(); - }; -} - diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ServerCommandInstance.cpp --- a/Resources/Graveyard/OldScheduler/ServerCommandInstance.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ServerCommandInstance.h" - -#include "../../Core/OrthancException.h" - -namespace Orthanc -{ - bool ServerCommandInstance::Execute(IListener& listener) - { - ListOfStrings outputs; - - bool success = false; - - try - { - if (command_->Apply(outputs, inputs_)) - { - success = true; - } - } - catch (OrthancException&) - { - } - - if (!success) - { - listener.SignalFailure(jobId_); - return true; - } - - for (std::list::iterator - it = next_.begin(); it != next_.end(); ++it) - { - for (ListOfStrings::const_iterator - output = outputs.begin(); output != outputs.end(); ++output) - { - (*it)->AddInput(*output); - } - } - - listener.SignalSuccess(jobId_); - return true; - } - - - ServerCommandInstance::ServerCommandInstance(IServerCommand *command, - const std::string& jobId) : - command_(command), - jobId_(jobId), - connectedToSink_(false) - { - if (command_ == NULL) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - - ServerCommandInstance::~ServerCommandInstance() - { - if (command_ != NULL) - { - delete command_; - } - } -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ServerCommandInstance.h --- a/Resources/Graveyard/OldScheduler/ServerCommandInstance.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,105 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../../Core/IDynamicObject.h" -#include "IServerCommand.h" - -namespace Orthanc -{ - class ServerCommandInstance : public IDynamicObject - { - friend class ServerScheduler; - - public: - class IListener - { - public: - virtual ~IListener() - { - } - - virtual void SignalSuccess(const std::string& jobId) = 0; - - virtual void SignalFailure(const std::string& jobId) = 0; - }; - - private: - typedef IServerCommand::ListOfStrings ListOfStrings; - - IServerCommand *command_; - std::string jobId_; - ListOfStrings inputs_; - std::list next_; - bool connectedToSink_; - - bool Execute(IListener& listener); - - public: - ServerCommandInstance(IServerCommand *command, - const std::string& jobId); - - virtual ~ServerCommandInstance(); - - const std::string& GetJobId() const - { - return jobId_; - } - - void AddInput(const std::string& input) - { - inputs_.push_back(input); - } - - void ConnectOutput(ServerCommandInstance& next) - { - next_.push_back(&next); - } - - void SetConnectedToSink(bool connected = true) - { - connectedToSink_ = connected; - } - - bool IsConnectedToSink() const - { - return connectedToSink_; - } - - const std::list& GetNextCommands() const - { - return next_; - } - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ServerJob.cpp --- a/Resources/Graveyard/OldScheduler/ServerJob.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,147 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ServerJob.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/Toolbox.h" - -namespace Orthanc -{ - void ServerJob::CheckOrdering() - { - std::map index; - - unsigned int count = 0; - for (std::list::const_iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - index[*it] = count++; - } - - for (std::list::const_iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - const std::list& nextCommands = (*it)->GetNextCommands(); - - for (std::list::const_iterator - next = nextCommands.begin(); next != nextCommands.end(); ++next) - { - if (index.find(*next) == index.end() || - index[*next] <= index[*it]) - { - // You must reorder your calls to "ServerJob::AddCommand" - throw OrthancException(ErrorCode_BadJobOrdering); - } - } - } - } - - - size_t ServerJob::Submit(SharedMessageQueue& target, - ServerCommandInstance::IListener& listener) - { - if (submitted_) - { - // This job has already been submitted - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - CheckOrdering(); - - size_t size = filters_.size(); - - for (std::list::iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - target.Enqueue(*it); - } - - filters_.clear(); - submitted_ = true; - - return size; - } - - - ServerJob::ServerJob() : - jobId_(Toolbox::GenerateUuid()), - submitted_(false), - description_("no description") - { - } - - - ServerJob::~ServerJob() - { - for (std::list::iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - delete *it; - } - - for (std::list::iterator - it = payloads_.begin(); it != payloads_.end(); ++it) - { - delete *it; - } - } - - - ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter) - { - if (submitted_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - filters_.push_back(new ServerCommandInstance(filter, jobId_)); - - return *filters_.back(); - } - - - IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload) - { - if (submitted_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - payloads_.push_back(payload); - - return *filters_.back(); - } - -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ServerJob.h --- a/Resources/Graveyard/OldScheduler/ServerJob.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ServerCommandInstance.h" -#include "../../Core/MultiThreading/SharedMessageQueue.h" - -namespace Orthanc -{ - class ServerJob - { - friend class ServerScheduler; - - private: - std::list filters_; - std::list payloads_; - std::string jobId_; - bool submitted_; - std::string description_; - - void CheckOrdering(); - - size_t Submit(SharedMessageQueue& target, - ServerCommandInstance::IListener& listener); - - public: - ServerJob(); - - ~ServerJob(); - - const std::string& GetId() const - { - return jobId_; - } - - void SetDescription(const std::string& description) - { - description_ = description; - } - - const std::string& GetDescription() const - { - return description_; - } - - ServerCommandInstance& AddCommand(IServerCommand* filter); - - // Take the ownership of a payload to a job. This payload will be - // automatically freed when the job succeeds or fails. - IDynamicObject& AddPayload(IDynamicObject* payload); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ServerScheduler.cpp --- a/Resources/Graveyard/OldScheduler/ServerScheduler.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,359 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ServerScheduler.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/Logging.h" - -namespace Orthanc -{ - namespace - { - // Anonymous namespace to avoid clashes between compilation modules - class Sink : public IServerCommand - { - private: - ListOfStrings& target_; - - public: - explicit Sink(ListOfStrings& target) : target_(target) - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - target_.push_back(*it); - } - - return true; - } - }; - } - - - ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId) - { - Jobs::iterator info = jobs_.find(jobId); - - if (info == jobs_.end()) - { - throw OrthancException(ErrorCode_InternalError); - } - - return info->second; - } - - - void ServerScheduler::SignalSuccess(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - JobInfo& info = GetJobInfo(jobId); - info.success_++; - - assert(info.failures_ == 0); - - if (info.success_ >= info.size_) - { - if (info.watched_) - { - watchedJobStatus_[jobId] = JobStatus_Success; - watchedJobFinished_.notify_all(); - } - - LOG(INFO) << "Job successfully finished (" << info.description_ << ")"; - jobs_.erase(jobId); - - availableJob_.Release(); - } - } - - - void ServerScheduler::SignalFailure(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - JobInfo& info = GetJobInfo(jobId); - info.failures_++; - - if (info.success_ + info.failures_ >= info.size_) - { - if (info.watched_) - { - watchedJobStatus_[jobId] = JobStatus_Failure; - watchedJobFinished_.notify_all(); - } - - LOG(ERROR) << "Job has failed (" << info.description_ << ")"; - jobs_.erase(jobId); - - availableJob_.Release(); - } - } - - - void ServerScheduler::Worker(ServerScheduler* that) - { - static const int32_t TIMEOUT = 100; - - LOG(WARNING) << "The server scheduler has started"; - - while (!that->finish_) - { - std::auto_ptr object(that->queue_.Dequeue(TIMEOUT)); - if (object.get() != NULL) - { - ServerCommandInstance& filter = dynamic_cast(*object); - - // Skip the execution of this filter if its parent job has - // previously failed. - bool jobHasFailed; - { - boost::mutex::scoped_lock lock(that->mutex_); - JobInfo& info = that->GetJobInfo(filter.GetJobId()); - jobHasFailed = (info.failures_ > 0 || info.cancel_); - } - - if (jobHasFailed) - { - that->SignalFailure(filter.GetJobId()); - } - else - { - filter.Execute(*that); - } - } - } - } - - - void ServerScheduler::SubmitInternal(ServerJob& job, - bool watched) - { - availableJob_.Acquire(); - - boost::mutex::scoped_lock lock(mutex_); - - JobInfo info; - info.size_ = job.Submit(queue_, *this); - info.cancel_ = false; - info.success_ = 0; - info.failures_ = 0; - info.description_ = job.GetDescription(); - info.watched_ = watched; - - assert(info.size_ > 0); - - if (watched) - { - watchedJobStatus_[job.GetId()] = JobStatus_Running; - } - - jobs_[job.GetId()] = info; - - LOG(INFO) << "New job submitted (" << job.description_ << ")"; - } - - - ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs) - { - if (maxJobs == 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - finish_ = false; - worker_ = boost::thread(Worker, this); - } - - - ServerScheduler::~ServerScheduler() - { - if (!finish_) - { - LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!"; - Stop(); - } - } - - - void ServerScheduler::Stop() - { - if (!finish_) - { - finish_ = true; - - if (worker_.joinable()) - { - worker_.join(); - } - } - } - - - void ServerScheduler::Submit(ServerJob& job) - { - if (job.filters_.empty()) - { - return; - } - - SubmitInternal(job, false); - } - - - bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs, - ServerJob& job) - { - std::string jobId = job.GetId(); - - outputs.clear(); - - if (job.filters_.empty()) - { - return true; - } - - // Add a sink filter to collect all the results of the filters - // that have no next filter. - ServerCommandInstance& sink = job.AddCommand(new Sink(outputs)); - - for (std::list::iterator - it = job.filters_.begin(); it != job.filters_.end(); ++it) - { - if ((*it) != &sink && - (*it)->IsConnectedToSink()) - { - (*it)->ConnectOutput(sink); - } - } - - // Submit the job - SubmitInternal(job, true); - - // Wait for the job to complete (either success or failure) - JobStatus status; - - { - boost::mutex::scoped_lock lock(mutex_); - - assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end()); - - while (watchedJobStatus_[jobId] == JobStatus_Running) - { - watchedJobFinished_.wait(lock); - } - - status = watchedJobStatus_[jobId]; - watchedJobStatus_.erase(jobId); - } - - return (status == JobStatus_Success); - } - - - bool ServerScheduler::SubmitAndWait(ServerJob& job) - { - ListOfStrings ignoredSink; - return SubmitAndWait(ignoredSink, job); - } - - - bool ServerScheduler::IsRunning(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - return jobs_.find(jobId) != jobs_.end(); - } - - - void ServerScheduler::Cancel(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - Jobs::iterator job = jobs_.find(jobId); - - if (job != jobs_.end()) - { - job->second.cancel_ = true; - LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")"; - } - } - - - float ServerScheduler::GetProgress(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - Jobs::iterator job = jobs_.find(jobId); - - if (job == jobs_.end() || - job->second.size_ == 0 /* should never happen */) - { - // This job is not running - return 1; - } - - if (job->second.failures_ != 0) - { - return 1; - } - - if (job->second.size_ == 1) - { - return static_cast(job->second.success_); - } - - return (static_cast(job->second.success_) / - static_cast(job->second.size_ - 1)); - } - - - void ServerScheduler::GetListOfJobs(ListOfStrings& jobs) - { - boost::mutex::scoped_lock lock(mutex_); - - jobs.clear(); - - for (Jobs::const_iterator - it = jobs_.begin(); it != jobs_.end(); ++it) - { - jobs.push_back(it->first); - } - } -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/ServerScheduler.h --- a/Resources/Graveyard/OldScheduler/ServerScheduler.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,123 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ServerJob.h" - -#include "../../Core/MultiThreading/Semaphore.h" - -namespace Orthanc -{ - class ServerScheduler : public ServerCommandInstance::IListener - { - private: - struct JobInfo - { - bool watched_; - bool cancel_; - size_t size_; - size_t success_; - size_t failures_; - std::string description_; - }; - - enum JobStatus - { - JobStatus_Running = 1, - JobStatus_Success = 2, - JobStatus_Failure = 3 - }; - - typedef IServerCommand::ListOfStrings ListOfStrings; - typedef std::map Jobs; - - boost::mutex mutex_; - boost::condition_variable watchedJobFinished_; - Jobs jobs_; - SharedMessageQueue queue_; - bool finish_; - boost::thread worker_; - std::map watchedJobStatus_; - Semaphore availableJob_; - - JobInfo& GetJobInfo(const std::string& jobId); - - virtual void SignalSuccess(const std::string& jobId); - - virtual void SignalFailure(const std::string& jobId); - - static void Worker(ServerScheduler* that); - - void SubmitInternal(ServerJob& job, - bool watched); - - public: - explicit ServerScheduler(unsigned int maxjobs); - - ~ServerScheduler(); - - void Stop(); - - void Submit(ServerJob& job); - - bool SubmitAndWait(ListOfStrings& outputs, - ServerJob& job); - - bool SubmitAndWait(ServerJob& job); - - bool IsRunning(const std::string& jobId); - - void Cancel(const std::string& jobId); - - // Returns a number between 0 and 1 - float GetProgress(const std::string& jobId); - - bool IsRunning(const ServerJob& job) - { - return IsRunning(job.GetId()); - } - - void Cancel(const ServerJob& job) - { - Cancel(job.GetId()); - } - - float GetProgress(const ServerJob& job) - { - return GetProgress(job.GetId()); - } - - void GetListOfJobs(ListOfStrings& jobs); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/StorePeerCommand.cpp --- a/Resources/Graveyard/OldScheduler/StorePeerCommand.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "StorePeerCommand.h" - -#include "../../Core/Logging.h" -#include "../../Core/HttpClient.h" - -namespace Orthanc -{ - StorePeerCommand::StorePeerCommand(ServerContext& context, - const WebServiceParameters& peer, - bool ignoreExceptions) : - context_(context), - peer_(peer), - ignoreExceptions_(ignoreExceptions) - { - } - - bool StorePeerCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - // Configure the HTTP client - HttpClient client(peer_, "instances"); - client.SetMethod(HttpMethod_Post); - - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to peer \"" - << peer_.GetUrl() << "\""; - - try - { - context_.ReadDicom(client.GetBody(), *it); - - std::string answer; - if (!client.Apply(answer)) - { - LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\""; - throw OrthancException(ErrorCode_NetworkProtocol); - } - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to forward to an Orthanc peer in (instance " - << *it << ", peer " << peer_.GetUrl() << "): " << e.What(); - - if (!ignoreExceptions_) - { - throw; - } - } - } - - return true; - } -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/StorePeerCommand.h --- a/Resources/Graveyard/OldScheduler/StorePeerCommand.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" -#include "../OrthancInitialization.h" - -namespace Orthanc -{ - class StorePeerCommand : public IServerCommand - { - private: - ServerContext& context_; - WebServiceParameters peer_; - bool ignoreExceptions_; - - public: - StorePeerCommand(ServerContext& context, - const WebServiceParameters& peer, - bool ignoreExceptions); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/StoreScuCommand.cpp --- a/Resources/Graveyard/OldScheduler/StoreScuCommand.cpp Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "StoreScuCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - StoreScuCommand::StoreScuCommand(ServerContext& context, - const std::string& localAet, - const RemoteModalityParameters& modality, - bool ignoreExceptions) : - context_(context), - modality_(modality), - ignoreExceptions_(ignoreExceptions), - localAet_(localAet), - moveOriginatorID_(0) - { - } - - - void StoreScuCommand::SetMoveOriginator(const std::string& aet, - uint16_t id) - { - moveOriginatorAET_ = aet; - moveOriginatorID_ = id; - } - - - bool StoreScuCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); - - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to modality \"" - << modality_.GetApplicationEntityTitle() << "\""; - - try - { - std::string dicom; - context_.ReadDicom(dicom, *it); - - locker.GetConnection().Store(dicom, moveOriginatorAET_, moveOriginatorID_); - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - // Ignore transmission errors (e.g. if the remote modality is - // powered off) - LOG(ERROR) << "Unable to forward to a modality in (instance " - << *it << "): " << e.What(); - - if (!ignoreExceptions_) - { - throw; - } - } - } - - return true; - } -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/OldScheduler/StoreScuCommand.h --- a/Resources/Graveyard/OldScheduler/StoreScuCommand.h Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class StoreScuCommand : public IServerCommand - { - private: - ServerContext& context_; - RemoteModalityParameters modality_; - bool ignoreExceptions_; - std::string localAet_; - std::string moveOriginatorAET_; - uint16_t moveOriginatorID_; - - public: - StoreScuCommand(ServerContext& context, - const std::string& localAet, - const RemoteModalityParameters& modality, - bool ignoreExceptions); - - void SetMoveOriginator(const std::string& aet, - uint16_t id); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r dba48c162b7b -r 82e88ff003d7 Resources/Graveyard/TestTranscoding.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/TestTranscoding.cpp Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,967 @@ + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmFileFormat& dicom, + DicomTransferSyntax syntax) + { + E_TransferSyntax xfer; + if (!LookupDcmtkTransferSyntax(xfer, syntax)) + { + return false; + } + else if (!dicom.validateMetaInfo(xfer).good()) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot setup the transfer syntax to write a DICOM instance"); + } + else + { + return SaveToMemoryBufferInternal(buffer, dicom, xfer); + } + } + + + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmFileFormat& dicom) + { + E_TransferSyntax xfer = dicom.getDataset()->getCurrentXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot write a DICOM instance with unknown transfer syntax"); + } + else if (!dicom.validateMetaInfo(xfer).good()) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot setup the transfer syntax to write a DICOM instance"); + } + else + { + return SaveToMemoryBufferInternal(buffer, dicom, xfer); + } + } + + + + + +#include +#include +#include + +#include "../Core/DicomParsing/Internals/DicomFrameIndex.h" + +namespace Orthanc +{ + class IParsedDicomImage : public boost::noncopyable + { + public: + virtual ~IParsedDicomImage() + { + } + + virtual DicomTransferSyntax GetTransferSyntax() = 0; + + virtual std::string GetSopClassUid() = 0; + + virtual std::string GetSopInstanceUid() = 0; + + virtual unsigned int GetFramesCount() = 0; + + // Can return NULL, for compressed transfer syntaxes + virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) = 0; + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) = 0; + + virtual void WriteToMemoryBuffer(std::string& target) = 0; + }; + + + class IDicomImageReader : public boost::noncopyable + { + public: + virtual ~IDicomImageReader() + { + } + + virtual IParsedDicomImage* Read(const void* data, + size_t size) = 0; + + virtual IParsedDicomImage* Transcode(const void* data, + size_t size, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) = 0; + }; + + + class DcmtkImageReader : public IDicomImageReader + { + private: + class Image : public IParsedDicomImage + { + private: + std::unique_ptr dicom_; + std::unique_ptr index_; + DicomTransferSyntax transferSyntax_; + std::string sopClassUid_; + std::string sopInstanceUid_; + + static std::string GetStringTag(DcmDataset& dataset, + const DcmTagKey& tag) + { + const char* value = NULL; + + if (!dataset.findAndGetString(tag, value).good() || + value == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing SOP class/instance UID in DICOM instance"); + } + else + { + return std::string(value); + } + } + + public: + Image(DcmFileFormat* dicom, + DicomTransferSyntax syntax) : + dicom_(dicom), + transferSyntax_(syntax) + { + if (dicom == NULL || + dicom_->getDataset() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DcmDataset& dataset = *dicom_->getDataset(); + index_.reset(new DicomFrameIndex(dataset)); + + sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID); + sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID); + } + + virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE + { + return transferSyntax_; + } + + virtual std::string GetSopClassUid() ORTHANC_OVERRIDE + { + return sopClassUid_; + } + + virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE + { + return sopInstanceUid_; + } + + virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE + { + return index_->GetFramesCount(); + } + + virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE + { + assert(dicom_.get() != NULL); + if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, transferSyntax_)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot write the DICOM instance to a memory buffer"); + } + } + + virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) ORTHANC_OVERRIDE + { + assert(dicom_.get() != NULL && + dicom_->getDataset() != NULL); + return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); + } + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) ORTHANC_OVERRIDE + { + assert(index_.get() != NULL); + index_->GetRawFrame(target, frame); + } + }; + + unsigned int lossyQuality_; + + static DicomTransferSyntax DetectTransferSyntax(DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DcmDataset& dataset = *dicom.getDataset(); + + E_TransferSyntax xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + dataset.updateOriginalXfer(); + xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax of the DICOM instance"); + } + } + + DicomTransferSyntax syntax; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, xfer)) + { + return syntax; + } + else + { + throw OrthancException( + ErrorCode_BadFileFormat, + "Unsupported transfer syntax: " + boost::lexical_cast(xfer)); + } + } + + + static uint16_t GetBitsStored(DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + uint16_t bitsStored; + if (dicom.getDataset()->findAndGetUint16(DCM_BitsStored, bitsStored).good()) + { + return bitsStored; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing \"Bits Stored\" tag in DICOM instance"); + } + } + + + public: + DcmtkImageReader() : + lossyQuality_(90) + { + } + + void SetLossyQuality(unsigned int quality) + { + if (quality <= 0 || + quality > 100) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + lossyQuality_ = quality; + } + } + + unsigned int GetLossyQuality() const + { + return lossyQuality_; + } + + virtual IParsedDicomImage* Read(const void* data, + size_t size) + { + std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size)); + if (dicom.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + DicomTransferSyntax transferSyntax = DetectTransferSyntax(*dicom); + + return new Image(dicom.release(), transferSyntax); + } + + virtual IParsedDicomImage* Transcode(const void* data, + size_t size, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) + { + std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size)); + if (dicom.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const uint16_t bitsStored = GetBitsStored(*dicom); + + if (syntax == DetectTransferSyntax(*dicom)) + { + // No transcoding is needed + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_LittleEndianImplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianImplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_LittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianExplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_BigEndianExplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_BigEndianExplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess1 && + allowNewSopInstanceUid && + bitsStored == 8) + { + DJ_RPLossy rpLossy(lossyQuality_); + + if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess1, &rpLossy)) + { + return new Image(dicom.release(), syntax); + } + } +#endif + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess2_4 && + allowNewSopInstanceUid && + bitsStored <= 12) + { + DJ_RPLossy rpLossy(lossyQuality_); + if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess2_4, &rpLossy)) + { + return new Image(dicom.release(), syntax); + } + } +#endif + + //LOG(INFO) << "Unable to transcode DICOM image using the built-in reader"; + return NULL; + } + }; + + + + class IDicomTranscoder1 : public boost::noncopyable + { + public: + virtual ~IDicomTranscoder1() + { + } + + virtual DcmFileFormat& GetDicom() = 0; + + virtual DicomTransferSyntax GetTransferSyntax() = 0; + + virtual std::string GetSopClassUid() = 0; + + virtual std::string GetSopInstanceUid() = 0; + + virtual unsigned int GetFramesCount() = 0; + + virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0; + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) = 0; + + // NB: Transcoding can change the value of "GetSopInstanceUid()" + // and "GetTransferSyntax()" if lossy compression is applied + virtual bool Transcode(std::string& target, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) = 0; + + virtual void WriteToMemoryBuffer(std::string& target) = 0; + }; + + + class DcmtkTranscoder2 : public IDicomTranscoder1 + { + private: + std::unique_ptr dicom_; + std::unique_ptr index_; + DicomTransferSyntax transferSyntax_; + std::string sopClassUid_; + std::string sopInstanceUid_; + uint16_t bitsStored_; + unsigned int lossyQuality_; + + static std::string GetStringTag(DcmDataset& dataset, + const DcmTagKey& tag) + { + const char* value = NULL; + + if (!dataset.findAndGetString(tag, value).good() || + value == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing SOP class/instance UID in DICOM instance"); + } + else + { + return std::string(value); + } + } + + void Setup(DcmFileFormat* dicom) + { + lossyQuality_ = 90; + + dicom_.reset(dicom); + + if (dicom == NULL || + dicom_->getDataset() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DcmDataset& dataset = *dicom_->getDataset(); + index_.reset(new DicomFrameIndex(dataset)); + + E_TransferSyntax xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + dataset.updateOriginalXfer(); + xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax of the DICOM instance"); + } + } + + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer)) + { + throw OrthancException( + ErrorCode_BadFileFormat, + "Unsupported transfer syntax: " + boost::lexical_cast(xfer)); + } + + if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored_).good()) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing \"Bits Stored\" tag in DICOM instance"); + } + + sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID); + sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID); + } + + public: + DcmtkTranscoder2(DcmFileFormat* dicom) // Takes ownership + { + Setup(dicom); + } + + DcmtkTranscoder2(const void* dicom, + size_t size) + { + Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size)); + } + + void SetLossyQuality(unsigned int quality) + { + if (quality <= 0 || + quality > 100) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + lossyQuality_ = quality; + } + } + + unsigned int GetLossyQuality() const + { + return lossyQuality_; + } + + unsigned int GetBitsStored() const + { + return bitsStored_; + } + + virtual DcmFileFormat& GetDicom() + { + assert(dicom_ != NULL); + return *dicom_; + } + + virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE + { + return transferSyntax_; + } + + virtual std::string GetSopClassUid() ORTHANC_OVERRIDE + { + return sopClassUid_; + } + + virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE + { + return sopInstanceUid_; + } + + virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE + { + return index_->GetFramesCount(); + } + + virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE + { + if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot write the DICOM instance to a memory buffer"); + } + } + + virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE + { + assert(dicom_->getDataset() != NULL); + return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); + } + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) ORTHANC_OVERRIDE + { + index_->GetRawFrame(target, frame); + } + + virtual bool Transcode(std::string& target, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE + { + assert(dicom_ != NULL && + dicom_->getDataset() != NULL); + + if (syntax == GetTransferSyntax()) + { + printf("NO TRANSCODING\n"); + + // No change in the transfer syntax => simply serialize the current dataset + WriteToMemoryBuffer(target); + return true; + } + + printf(">> %d\n", bitsStored_); + + if (syntax == DicomTransferSyntax_LittleEndianImplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_LittleEndianImplicit; + return true; + } + + if (syntax == DicomTransferSyntax_LittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_LittleEndianExplicit; + return true; + } + + if (syntax == DicomTransferSyntax_BigEndianExplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_BigEndianExplicit; + return true; + } + + if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_DeflatedLittleEndianExplicit; + return true; + } + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess1 && + allowNewSopInstanceUid && + GetBitsStored() == 8) + { + DJ_RPLossy rpLossy(lossyQuality_); + + if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_JPEGProcess1; + sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID); + return true; + } + } +#endif + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess2_4 && + allowNewSopInstanceUid && + GetBitsStored() <= 12) + { + DJ_RPLossy rpLossy(lossyQuality_); + if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_JPEGProcess2_4; + sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID); + return true; + } + } +#endif + + return false; + } + }; +} + + + + +#include + + +static void TestFile(const std::string& path) +{ + static unsigned int count = 0; + count++; + + + printf("** %s\n", path.c_str()); + + std::string s; + SystemToolbox::ReadFile(s, path); + + Orthanc::DcmtkTranscoder2 transcoder(s.c_str(), s.size()); + + /*if (transcoder.GetBitsStored() != 8) // TODO + return; */ + + { + char buf[1024]; + sprintf(buf, "/tmp/source-%06d.dcm", count); + printf(">> %s\n", buf); + Orthanc::SystemToolbox::WriteFile(s, buf); + } + + printf("[%s] [%s] [%s] %d %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()), + transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(), + transcoder.GetFramesCount(), transcoder.GetTransferSyntax()); + + for (size_t i = 0; i < transcoder.GetFramesCount(); i++) + { + std::string f; + transcoder.GetCompressedFrame(f, i); + + if (i == 0) + { + char buf[1024]; + sprintf(buf, "/tmp/frame-%06d.raw", count); + printf(">> %s\n", buf); + Orthanc::SystemToolbox::WriteFile(f, buf); + } + } + + { + std::string t; + transcoder.WriteToMemoryBuffer(t); + + Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size()); + printf(">> %d %d ; %lu bytes\n", transcoder.GetTransferSyntax(), transcoder2.GetTransferSyntax(), t.size()); + } + + { + std::string a = transcoder.GetSopInstanceUid(); + DicomTransferSyntax b = transcoder.GetTransferSyntax(); + + DicomTransferSyntax syntax = DicomTransferSyntax_JPEGProcess2_4; + //DicomTransferSyntax syntax = DicomTransferSyntax_LittleEndianExplicit; + + std::string t; + bool ok = transcoder.Transcode(t, syntax, true); + printf("Transcoding: %d\n", ok); + + if (ok) + { + printf("[%s] => [%s]\n", a.c_str(), transcoder.GetSopInstanceUid().c_str()); + printf("[%s] => [%s]\n", GetTransferSyntaxUid(b), + GetTransferSyntaxUid(transcoder.GetTransferSyntax())); + + { + char buf[1024]; + sprintf(buf, "/tmp/transcoded-%06d.dcm", count); + printf(">> %s\n", buf); + Orthanc::SystemToolbox::WriteFile(t, buf); + } + + Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size()); + printf(" => transcoded transfer syntax %d ; %lu bytes\n", transcoder2.GetTransferSyntax(), t.size()); + } + } + + printf("\n"); +} + +TEST(Toto, DISABLED_Transcode) +{ + //OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); + + if (1) + { + const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes"; + + for (boost::filesystem::directory_iterator it(PATH); + it != boost::filesystem::directory_iterator(); ++it) + { + if (boost::filesystem::is_regular_file(it->status())) + { + TestFile(it->path().string()); + } + } + } + + if (0) + { + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm"); + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm"); + } + + if (0) + { + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm"); + } +} + + +TEST(Toto, DISABLED_Transcode2) +{ + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string(GetTransferSyntaxUid(a)) + ".dcm"); + if (Orthanc::SystemToolbox::IsRegularFile(path)) + { + printf("\n======= %s\n", GetTransferSyntaxUid(a)); + + std::string source; + Orthanc::SystemToolbox::ReadFile(source, path); + + DcmtkImageReader reader; + + { + std::unique_ptr image( + reader.Read(source.c_str(), source.size())); + ASSERT_TRUE(image.get() != NULL); + ASSERT_EQ(a, image->GetTransferSyntax()); + + std::string target; + image->WriteToMemoryBuffer(target); + } + + for (int j = 0; j <= DicomTransferSyntax_XML; j++) + { + DicomTransferSyntax b = (DicomTransferSyntax) j; + //if (a == b) continue; + + std::unique_ptr image( + reader.Transcode(source.c_str(), source.size(), b, true)); + if (image.get() != NULL) + { + printf("[%s] -> [%s]\n", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b)); + + std::string target; + image->WriteToMemoryBuffer(target); + + char buf[1024]; + sprintf(buf, "/tmp/%s-%s.dcm", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b)); + + SystemToolbox::WriteFile(target, buf); + } + else if (a != DicomTransferSyntax_JPEG2000 && + a != DicomTransferSyntax_JPEG2000LosslessOnly) + { + ASSERT_TRUE(b != DicomTransferSyntax_LittleEndianImplicit && + b != DicomTransferSyntax_LittleEndianExplicit && + b != DicomTransferSyntax_BigEndianExplicit && + b != DicomTransferSyntax_DeflatedLittleEndianExplicit); + } + } + } + } +} + + +#include "../Core/DicomNetworking/DicomAssociation.h" +#include "../Core/DicomNetworking/DicomControlUserConnection.h" +#include "../Core/DicomNetworking/DicomStoreUserConnection.h" + +TEST(Toto, DISABLED_DicomAssociation) +{ + DicomAssociationParameters params; + params.SetLocalApplicationEntityTitle("ORTHANC"); + params.SetRemoteApplicationEntityTitle("PACS"); + params.SetRemotePort(2001); + +#if 0 + DicomAssociation assoc; + assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass); + assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEGProcess1); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEGProcess2_4); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEG2000); + + assoc.Open(params); + + int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage); + printf(">> %d\n", presID); + + std::map pc; + printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage)); + + for (std::map::const_iterator + it = pc.begin(); it != pc.end(); ++it) + { + printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second); + } +#else + { + DicomControlUserConnection assoc(params); + + try + { + printf(">> %d\n", assoc.Echo()); + } + catch (OrthancException&) + { + } + } + + params.SetRemoteApplicationEntityTitle("PACS"); + params.SetRemotePort(2000); + + { + DicomControlUserConnection assoc(params); + printf(">> %d\n", assoc.Echo()); + } + +#endif +} + +static void TestTranscode(DicomStoreUserConnection& scu, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax) +{ + std::set accepted; + + scu.LookupTranscoding(accepted, sopClassUid, transferSyntax); + if (accepted.empty()) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "The SOP class is not supported by the remote modality"); + } + + { + unsigned int count = 0; + for (std::set::const_iterator + it = accepted.begin(); it != accepted.end(); ++it) + { + LOG(INFO) << "available for transcoding " << (count++) << ": " << sopClassUid + << " / " << GetTransferSyntaxUid(*it); + } + } + + if (accepted.find(transferSyntax) != accepted.end()) + { + printf("**** OK, without transcoding !! [%s]\n", GetTransferSyntaxUid(transferSyntax)); + } + else + { + // Transcoding - only in Orthanc >= 1.7.0 + + const DicomTransferSyntax uncompressed[] = { + DicomTransferSyntax_LittleEndianImplicit, // Default transfer syntax + DicomTransferSyntax_LittleEndianExplicit, + DicomTransferSyntax_BigEndianExplicit + }; + + bool found = false; + for (size_t i = 0; i < 3; i++) + { + if (accepted.find(uncompressed[i]) != accepted.end()) + { + printf("**** TRANSCODING to %s\n", GetTransferSyntaxUid(uncompressed[i])); + found = true; + break; + } + } + + if (!found) + { + printf("**** KO KO KO\n"); + } + } +} + + +TEST(Toto, DISABLED_Store) +{ + DicomAssociationParameters params; + params.SetLocalApplicationEntityTitle("ORTHANC"); + params.SetRemoteApplicationEntityTitle("STORESCP"); + params.SetRemotePort(2000); + + DicomStoreUserConnection assoc(params); + assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess1); + assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4); + //assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); + + //assoc.SetUncompressedSyntaxesProposed(false); // Necessary for transcoding + assoc.SetCommonClassesProposed(false); + assoc.SetRetiredBigEndianProposed(true); + TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); + TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000); + TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000); +} + + +TEST(Toto, DISABLED_Store2) +{ + DicomAssociationParameters params; + params.SetLocalApplicationEntityTitle("ORTHANC"); + params.SetRemoteApplicationEntityTitle("STORESCP"); + params.SetRemotePort(2000); + + DicomStoreUserConnection assoc(params); + //assoc.SetCommonClassesProposed(false); + assoc.SetRetiredBigEndianProposed(true); + + std::string s; + Orthanc::SystemToolbox::ReadFile(s, "/tmp/i/" + std::string(GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit)) +".dcm"); + + std::string c, i; + assoc.Store(c, i, s.c_str(), s.size()); + printf("[%s] [%s]\n", c.c_str(), i.c_str()); +} + diff -r dba48c162b7b -r 82e88ff003d7 Resources/Patches/openssl-1.1.1d.patch --- a/Resources/Patches/openssl-1.1.1d.patch Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -diff -urEb openssl-1.1.1d.orig/crypto/rand/rand_unix.c openssl-1.1.1d/crypto/rand/rand_unix.c ---- openssl-1.1.1d.orig/crypto/rand/rand_unix.c 2019-09-10 15:13:07.000000000 +0200 -+++ openssl-1.1.1d/crypto/rand/rand_unix.c 2020-03-05 16:29:33.030136203 +0100 -@@ -340,7 +340,7 @@ - # endif - - /* Linux supports this since version 3.17 */ --# if defined(__linux) && defined(__NR_getrandom) -+# if defined(__linux) && defined(__NR_getrandom) && !defined(__LSB_VERSION__) - return syscall(__NR_getrandom, buf, buflen, 0); - # elif (defined(__FreeBSD__) || defined(__NetBSD__)) && defined(KERN_ARND) - return sysctl_random(buf, buflen); diff -r dba48c162b7b -r 82e88ff003d7 Resources/Patches/openssl-1.1.1f.patch --- a/Resources/Patches/openssl-1.1.1f.patch Tue May 12 07:50:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -diff -urEb openssl-1.1.1f.orig/crypto/rand/rand_unix.c openssl-1.1.1f/crypto/rand/rand_unix.c ---- openssl-1.1.1f.orig/crypto/rand/rand_unix.c 2020-03-31 14:17:45.000000000 +0200 -+++ openssl-1.1.1f/crypto/rand/rand_unix.c 2020-04-02 16:38:56.091240847 +0200 -@@ -445,6 +445,7 @@ - * system call and this should always succeed which renders - * this alternative but essentially identical source moot. - */ -+#if !defined(__LSB_VERSION__) // "syscall()" is not available in LSB - if (uname(&un) == 0) { - kernel[0] = atoi(un.release); - p = strchr(un.release, '.'); -@@ -455,6 +456,7 @@ - return 0; - } - } -+#endif - /* Open /dev/random and wait for it to be readable */ - if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) { - if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) { diff -r dba48c162b7b -r 82e88ff003d7 Resources/Patches/openssl-1.1.1g.patch --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Patches/openssl-1.1.1g.patch Tue May 12 14:58:24 2020 +0200 @@ -0,0 +1,19 @@ +diff -urEb openssl-1.1.1g.orig/crypto/rand/rand_unix.c openssl-1.1.1g/crypto/rand/rand_unix.c +--- openssl-1.1.1g.orig/crypto/rand/rand_unix.c 2020-05-05 17:58:08.785998440 +0200 ++++ openssl-1.1.1g/crypto/rand/rand_unix.c 2020-05-05 17:58:55.201881117 +0200 +@@ -445,6 +445,7 @@ + * system call and this should always succeed which renders + * this alternative but essentially identical source moot. + */ ++#if !defined(__LSB_VERSION__) // "syscall()" is not available in LSB + if (uname(&un) == 0) { + kernel[0] = atoi(un.release); + p = strchr(un.release, '.'); +@@ -455,6 +456,7 @@ + return 0; + } + } ++#endif + /* Open /dev/random and wait for it to be readable */ + if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) { + if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) { diff -r dba48c162b7b -r 82e88ff003d7 UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Tue May 12 07:50:38 2020 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Tue May 12 14:58:24 2020 +0200 @@ -1924,349 +1924,102 @@ #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 -#include "../Core/DicomParsing/Internals/DicomFrameIndex.h" - -#include -#include -#include - - -namespace Orthanc -{ - class IDicomTranscoder : public boost::noncopyable - { - public: - virtual ~IDicomTranscoder() - { - } +#include "../Core/DicomNetworking/DicomStoreUserConnection.h" +#include "../Core/DicomParsing/DcmtkTranscoder.h" - virtual DicomTransferSyntax GetTransferSyntax() = 0; - - virtual std::string GetSopClassUid() = 0; - - virtual std::string GetSopInstanceUid() = 0; - - virtual unsigned int GetFramesCount() = 0; - - virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0; - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) = 0; - - virtual IDicomTranscoder* Transcode(std::set syntaxes, - bool allowNewSopInstanceUid) = 0; - }; - +TEST(Toto, DISABLED_Transcode3) +{ + DicomAssociationParameters p; + p.SetRemotePort(2000); - class DcmtkTranscoder : public IDicomTranscoder - { - private: - std::unique_ptr dicom_; - std::unique_ptr index_; - DicomTransferSyntax transferSyntax_; - std::string sopClassUid_; - std::string sopInstanceUid_; - - void Setup(DcmFileFormat* dicom) - { - dicom_.reset(dicom); - - if (dicom == NULL || - dicom_->getDataset() == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - - DcmDataset& dataset = *dicom_->getDataset(); - index_.reset(new DicomFrameIndex(dataset)); + DicomStoreUserConnection scu(p); + scu.SetCommonClassesProposed(false); + scu.SetRetiredBigEndianProposed(true); - E_TransferSyntax xfer = dataset.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - dataset.updateOriginalXfer(); - xfer = dataset.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - throw OrthancException(ErrorCode_BadFileFormat, - "Cannot determine the transfer syntax of the DICOM instance"); - } - } + DcmtkTranscoder transcoder; - if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer)) - { - throw OrthancException( - ErrorCode_BadFileFormat, - "Unsupported transfer syntax: " + boost::lexical_cast(xfer)); - } - - const char* a = NULL; - const char* b = NULL; + for (int j = 0; j < 2; j++) + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; - if (!dataset.findAndGetString(DCM_SOPClassUID, a).good() || - !dataset.findAndGetString(DCM_SOPInstanceUID, b).good() || - a == NULL || - b == NULL) + std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string(GetTransferSyntaxUid(a)) + ".dcm"); + if (Orthanc::SystemToolbox::IsRegularFile(path)) { - throw OrthancException(ErrorCode_BadFileFormat, - "Missing SOP class/instance UID in DICOM instance"); - } - - sopClassUid_.assign(a); - sopInstanceUid_.assign(b); - } - - public: - DcmtkTranscoder(DcmFileFormat* dicom) // Takes ownership - { - Setup(dicom); - } + printf("\n======= %s\n", GetTransferSyntaxUid(a)); - DcmtkTranscoder(const void* dicom, - size_t size) - { - Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size)); - } - - virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE - { - return transferSyntax_; - } - - virtual std::string GetSopClassUid() ORTHANC_OVERRIDE - { - return sopClassUid_; - } - - virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE - { - return sopInstanceUid_; - } + std::string source; + Orthanc::SystemToolbox::ReadFile(source, path); - virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE - { - return index_->GetFramesCount(); - } - - virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE - { - assert(dicom_->getDataset() != NULL); - return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); - } - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) ORTHANC_OVERRIDE - { -#if 1 - index_->GetRawFrame(target, frame); - printf("%d: %d\n", frame, target.size()); -#endif - -#if 1 - assert(dicom_->getDataset() != NULL); - DcmDataset& dataset = *dicom_->getDataset(); - - DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset); - - if (pixelSequence != NULL && - frame == 0 && - pixelSequence->card() != GetFramesCount() + 1) - { - printf("COMPRESSED\n"); - - // Check out "djcodecd.cc" - - printf("%d fragments\n", pixelSequence->card()); - - // Skip the first fragment, that is the offset table - for (unsigned long i = 1; ;i++) + std::string c, i; + try { - DcmPixelItem *fragment = NULL; - if (pixelSequence->getItem(fragment, i).good()) + scu.Transcode(c, i, transcoder, source.c_str(), source.size(), false, "", 0); + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_NotImplemented) { - printf("fragment %d %d\n", i, fragment->getLength()); + LOG(ERROR) << "cannot transcode " << GetTransferSyntaxUid(a); } else { - break; + throw e; } } } -#endif } - - virtual IDicomTranscoder* Transcode(std::set syntaxes, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE - { - throw OrthancException(ErrorCode_NotImplemented); - } - }; } - - -static bool Transcode(std::string& buffer, - DcmDataset& dataSet, - E_TransferSyntax xfer) +TEST(Toto, DISABLED_Transcode4) { - // Determine the transfer syntax which shall be used to write the - // information to the file. We always switch to the Little Endian - // syntax, with explicit length. - - // http://support.dcmtk.org/docs/dcxfer_8h-source.html - - - /** - * Note that up to Orthanc 0.7.1 (inclusive), the - * "EXS_LittleEndianExplicit" was always used to save the DICOM - * dataset into memory. We now keep the original transfer syntax - * (if available). - **/ - //E_TransferSyntax xfer = dataSet.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - // No information about the original transfer syntax: This is - // most probably a DICOM dataset that was read from memory. - xfer = EXS_LittleEndianExplicit; - } - - E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; - - // Create the meta-header information - DcmFileFormat ff(&dataSet); - ff.validateMetaInfo(xfer); - ff.removeInvalidGroups(); - - // Create a memory buffer with the proper size - { - const uint32_t estimatedSize = ff.calcElementLength(xfer, encodingType); // (*) - buffer.resize(estimatedSize); - } - - DcmOutputBufferStream ob(&buffer[0], buffer.size()); - - // Fill the memory buffer with the meta-header and the dataset - ff.transferInit(); - OFCondition c = ff.write(ob, xfer, encodingType, NULL, - /*opt_groupLength*/ EGL_recalcGL, - /*opt_paddingType*/ EPD_withoutPadding); - ff.transferEnd(); - - if (c.good()) - { - // The DICOM file is successfully written, truncate the target - // buffer if its size was overestimated by (*) - ob.flush(); - - size_t effectiveSize = static_cast(ob.tell()); - if (effectiveSize < buffer.size()) - { - buffer.resize(effectiveSize); - } - - return true; - } - else - { - // Error - buffer.clear(); - return false; - } -} + std::string source; + Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm"); -#include "dcmtk/dcmjpeg/djrploss.h" /* for DJ_RPLossy */ -#include "dcmtk/dcmjpeg/djrplol.h" /* for DJ_RPLossless */ - -#include - - -static void TestFile(const std::string& path) -{ - printf("** %s\n", path.c_str()); - - std::string s; - SystemToolbox::ReadFile(s, path); - - Orthanc::DcmtkTranscoder transcoder(s.c_str(), s.size()); - - printf("[%s] [%s] [%s] %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()), - transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(), - transcoder.GetFramesCount()); - - for (size_t i = 0; i < transcoder.GetFramesCount(); i++) - { - std::string f; - transcoder.GetCompressedFrame(f, i); - - if (i == 0) - { - static unsigned int i = 0; - char buf[1024]; - sprintf(buf, "/tmp/frame-%06d.dcm", i++); - printf(">> %s\n", buf); - Orthanc::SystemToolbox::WriteFile(f, buf); - } - } + std::unique_ptr toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size())); + DicomTransferSyntax sourceSyntax; + ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto)); - printf("\n"); -} - -TEST(Toto, Transcode) -{ - if (0) - { - OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); - - std::string s; - //SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm"); - //SystemToolbox::ReadFile(s, "/home/jodogne/DICOM/Alain.dcm"); - SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/Brainix/Epi/IM-0001-0002.dcm"); - - std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(s.c_str(), s.size())); + DcmtkTranscoder transcoder; - // less /home/jodogne/Downloads/dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcxfer.h - printf(">> %d\n", dicom->getDataset()->getOriginalXfer()); // => 4 == EXS_JPEGProcess1 - - const DcmRepresentationParameter *p; - -#if 0 - E_TransferSyntax target = EXS_LittleEndianExplicit; - p = NULL; -#elif 1 - E_TransferSyntax target = EXS_JPEGProcess14SV1; - DJ_RPLossless rp_lossless(6, 0); - p = &rp_lossless; -#else - E_TransferSyntax target = EXS_JPEGProcess1; - DJ_RPLossy rp_lossy(90); // quality - p = &rp_lossy; -#endif - - ASSERT_TRUE(dicom->getDataset()->chooseRepresentation(target, p).good()); - ASSERT_TRUE(dicom->getDataset()->canWriteXfer(target)); + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; std::string t; - ASSERT_TRUE(Transcode(t, *dicom->getDataset(), target)); - SystemToolbox::WriteFile(s, "source.dcm"); - SystemToolbox::WriteFile(t, "target.dcm"); - } + bool hasSopInstanceUidChanged; + DicomTransferSyntax sourceSyntax2; - if (1) - { - const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes"; - - for (boost::filesystem::directory_iterator it(PATH); - it != boost::filesystem::directory_iterator(); ++it) + std::unique_ptr cloned(dynamic_cast(toto->clone())); + if (!transcoder.TranscodeParsedToBuffer(t, sourceSyntax2, hasSopInstanceUidChanged, *cloned, a, true)) + { + printf("**************** CANNOT: [%s] => [%s]\n", + GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a)); + } + else { - if (boost::filesystem::is_regular_file(it->status())) + DicomTransferSyntax targetSyntax; + ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, *cloned)); + + ASSERT_EQ(targetSyntax, a); + ASSERT_EQ(sourceSyntax, sourceSyntax2); + bool lossy = (a == DicomTransferSyntax_JPEGProcess1 || + a == DicomTransferSyntax_JPEGProcess2_4 || + a == DicomTransferSyntax_JPEGLSLossy); + + printf("SIZE: %lu\n", t.size()); + if (hasSopInstanceUidChanged) { - TestFile(it->path().string()); + ASSERT_TRUE(lossy); + } + else + { + ASSERT_FALSE(lossy); } } - - TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm"); - TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm"); } } diff -r dba48c162b7b -r 82e88ff003d7 UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Tue May 12 07:50:38 2020 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Tue May 12 14:58:24 2020 +0200 @@ -62,6 +62,7 @@ #include "../OrthancServer/ServerJobs/ArchiveJob.h" #include "../OrthancServer/ServerJobs/DicomModalityStoreJob.h" +#include "../OrthancServer/ServerJobs/DicomMoveScuJob.h" #include "../OrthancServer/ServerJobs/MergeStudyJob.h" #include "../OrthancServer/ServerJobs/OrthancPeerStoreJob.h" #include "../OrthancServer/ServerJobs/ResourceModificationJob.h" @@ -1314,7 +1315,7 @@ DicomInstanceToStore toStore; toStore.SetParsedDicomFile(dicom); - return (context_->Store(id, toStore) == StoreStatus_Success); + return (context_->Store(id, toStore, StoreInstanceMode_Default) == StoreStatus_Success); } }; } @@ -1414,7 +1415,7 @@ modality.SetPortNumber(1000); modality.SetManufacturer(ModalityManufacturer_StoreScp); - StoreScuOperation operation(luaManager, "TEST", modality); + StoreScuOperation operation(GetContext(), luaManager, "TEST", modality); ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); operation.Serialize(s); @@ -1514,11 +1515,11 @@ job.reset(unserializer.UnserializeJob(s)); DicomModalityStoreJob& tmp = dynamic_cast(*job); - ASSERT_EQ("LOCAL", tmp.GetLocalAet()); - ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle()); - ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost()); - ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber()); - ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetParameters().GetRemoteModality().GetManufacturer()); ASSERT_TRUE(tmp.HasMoveOriginator()); ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet()); ASSERT_EQ(42, tmp.GetMoveOriginatorId()); @@ -1902,6 +1903,7 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } s = Json::nullValue; @@ -1932,6 +1934,7 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } s["Port"] = "46"; @@ -1998,6 +2001,7 @@ ASSERT_EQ(104u, modality.GetPortNumber()); ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } { @@ -2007,6 +2011,7 @@ s["AET"] = "AET"; s["Host"] = "host"; s["Port"] = "104"; + s["AllowTranscoding"] = false; RemoteModalityParameters modality(s); ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); @@ -2015,6 +2020,7 @@ ASSERT_EQ(104u, modality.GetPortNumber()); ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_FALSE(modality.IsTranscodingAllowed()); } { @@ -2032,5 +2038,143 @@ ASSERT_EQ(104u, modality.GetPortNumber()); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } } + + +TEST_F(OrthancJobsSerialization, DicomAssociationParameters) +{ + Json::Value v; + + { + v = Json::objectValue; + DicomAssociationParameters p; + p.SerializeJob(v); + } + + { + DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v); + ASSERT_EQ("ORTHANC", p.GetLocalApplicationEntityTitle()); + ASSERT_EQ("ANY-SCP", p.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ(104u, p.GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("127.0.0.1", p.GetRemoteModality().GetHost()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), p.GetTimeout()); + } + + { + v = Json::objectValue; + DicomAssociationParameters p; + p.SetLocalApplicationEntityTitle("HELLO"); + p.SetRemoteApplicationEntityTitle("WORLD"); + p.SetRemotePort(42); + p.SetRemoteHost("MY_HOST"); + p.SetTimeout(43); + p.SerializeJob(v); + } + + { + DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v); + ASSERT_EQ("HELLO", p.GetLocalApplicationEntityTitle()); + ASSERT_EQ("WORLD", p.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ(42u, p.GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("MY_HOST", p.GetRemoteModality().GetHost()); + ASSERT_EQ(43u, p.GetTimeout()); + } + + { + DicomModalityStoreJob job(GetContext()); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout()); + ASSERT_FALSE(job->HasMoveOriginator()); + ASSERT_THROW(job->GetMoveOriginatorAet(), OrthancException); + ASSERT_THROW(job->GetMoveOriginatorId(), OrthancException); + ASSERT_FALSE(job->HasStorageCommitment()); + } + + { + RemoteModalityParameters r; + r.SetApplicationEntityTitle("HELLO"); + r.SetPortNumber(42); + r.SetHost("MY_HOST"); + + DicomModalityStoreJob job(GetContext()); + job.SetLocalAet("WORLD"); + job.SetRemoteModality(r); + job.SetTimeout(43); + job.SetMoveOriginator("ORIGINATOR", 100); + job.EnableStorageCommitment(true); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(43u, job->GetParameters().GetTimeout()); + ASSERT_TRUE(job->HasMoveOriginator()); + ASSERT_EQ("ORIGINATOR", job->GetMoveOriginatorAet()); + ASSERT_EQ(100, job->GetMoveOriginatorId()); + ASSERT_TRUE(job->HasStorageCommitment()); + } + + { + DicomMoveScuJob job(GetContext()); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout()); + } + + { + RemoteModalityParameters r; + r.SetApplicationEntityTitle("HELLO"); + r.SetPortNumber(42); + r.SetHost("MY_HOST"); + + DicomMoveScuJob job(GetContext()); + job.SetLocalAet("WORLD"); + job.SetRemoteModality(r); + job.SetTimeout(43); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(43u, job->GetParameters().GetTimeout()); + } +} diff -r dba48c162b7b -r 82e88ff003d7 UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Tue May 12 07:50:38 2020 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Tue May 12 14:58:24 2020 +0200 @@ -726,7 +726,8 @@ std::map instanceMetadata; DicomInstanceToStore toStore; toStore.SetSummary(instance); - ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments)); + ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments, + false /* don't overwrite */)); ASSERT_EQ(5u, instanceMetadata.size()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_RemoteAet) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end()); @@ -803,7 +804,7 @@ DicomInstanceHasher hasher(instance); std::string id = hasher.HashInstance(); - context.GetIndex().SetOverwriteInstances(overwrite); + context.SetOverwriteInstances(overwrite); uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances; context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, @@ -819,7 +820,7 @@ ASSERT_EQ(id, toStore.GetHasher().HashInstance()); std::string id2; - ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore)); + ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore, StoreInstanceMode_Default)); ASSERT_EQ(id, id2); } @@ -854,7 +855,8 @@ toStore.SetOrigin(DicomInstanceOrigin::FromPlugins()); std::string id2; - ASSERT_EQ(overwrite ? StoreStatus_Success : StoreStatus_AlreadyStored, context.Store(id2, toStore)); + ASSERT_EQ(overwrite ? StoreStatus_Success : StoreStatus_AlreadyStored, + context.Store(id2, toStore, StoreInstanceMode_Default)); ASSERT_EQ(id, id2); } diff -r dba48c162b7b -r 82e88ff003d7 UnitTestsSources/VersionsTests.cpp --- a/UnitTestsSources/VersionsTests.cpp Tue May 12 07:50:38 2020 +0200 +++ b/UnitTestsSources/VersionsTests.cpp Tue May 12 14:58:24 2020 +0200 @@ -185,7 +185,7 @@ TEST(Version, OpenSslStatic) { ASSERT_TRUE(OPENSSL_VERSION_NUMBER == 0x1000210fL /* openssl-1.0.2p */ || - OPENSSL_VERSION_NUMBER == 0x1010106fL /* openssl-1.1.1f */); + OPENSSL_VERSION_NUMBER == 0x1010107fL /* openssl-1.1.1g */); } #endif