# HG changeset patch # User Alain Mazy # Date 1585728933 -7200 # Node ID 320a2d2249024175d67100b72fa476ccb4f5ea3a # Parent c38b82bb6fd3e720f6dd6e4e617fcde723b505c1# Parent d73ce7c537c38147d7bc1f581b2f4be04e562b93 merge diff -r c38b82bb6fd3 -r 320a2d224902 AUTHORS --- a/AUTHORS Wed Apr 01 10:14:49 2020 +0200 +++ b/AUTHORS Wed Apr 01 10:15:33 2020 +0200 @@ -15,7 +15,7 @@ Belgium * Osimis S.A. - Rue du Bois Saint-Jean 15/1 - 4102 Seraing + Quai Banning 6 + 4000 Liege Belgium http://www.osimis.io/ diff -r c38b82bb6fd3 -r 320a2d224902 CMakeLists.txt --- a/CMakeLists.txt Wed Apr 01 10:14:49 2020 +0200 +++ b/CMakeLists.txt Wed Apr 01 10:15:33 2020 +0200 @@ -25,6 +25,9 @@ set(ENABLE_WEB_SERVER ON) set(ENABLE_ZLIB ON) +# To test transcoding +#set(ENABLE_DCMTK_TRANSCODING ON) + set(HAS_EMBEDDED_RESOURCES ON) @@ -103,8 +106,10 @@ OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp OrthancServer/ServerJobs/ResourceModificationJob.cpp OrthancServer/ServerJobs/SplitStudyJob.cpp + OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp OrthancServer/ServerToolbox.cpp OrthancServer/SliceOrdering.cpp + OrthancServer/StorageCommitmentReports.cpp ) @@ -483,9 +488,13 @@ ) endif() - externalproject_add(ConnectivityChecksProject + externalproject_add(ConnectivityChecks SOURCE_DIR "${ORTHANC_ROOT}/Plugins/Samples/ConnectivityChecks" + # We explicitly provide a build directory, in order to avoid paths + # that are too long on our Visual Studio 2008 CIS + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ConnectivityChecks-build" + CMAKE_ARGS -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR} diff -r c38b82bb6fd3 -r 320a2d224902 Core/Compatibility.h --- a/Core/Compatibility.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Compatibility.h Wed Apr 01 10:15:33 2020 +0200 @@ -47,17 +47,16 @@ // the compiler ("/Zc:__cplusplus") // To make this header more robust, we use the _MSVC_LANG equivalent macro. -# if _MSC_VER > 1900 -# if (defined _MSVC_LANG) && (_MSVC_LANG >= 201103L) -# define ORTHANC_Cxx03_DETECTED 0 -# else -# define ORTHANC_Cxx03_DETECTED 1 -# endif -# elif _MSC_VER > 1800 +// please note that not all C++11 features are supported when _MSC_VER == 1600 +// (or higher). This header file can be made for fine-grained, if required, +// based on specific _MSC_VER values + +# if _MSC_VER >= 1600 # define ORTHANC_Cxx03_DETECTED 0 # else # define ORTHANC_Cxx03_DETECTED 1 # endif + #else // of _MSC_VER is not defined, we assume __cplusplus is correctly defined // if __cplusplus is not defined (very old compilers??), then the following @@ -89,12 +88,12 @@ class unique_ptr : public boost::movelib::unique_ptr { public: - unique_ptr() : + explicit unique_ptr() : boost::movelib::unique_ptr() { } - unique_ptr(T* p) : + explicit unique_ptr(T* p) : boost::movelib::unique_ptr(p) { } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomFormat/DicomMap.cpp --- a/Core/DicomFormat/DicomMap.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomFormat/DicomMap.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -745,9 +745,8 @@ } - bool DicomMap::ParseDicomMetaInformation(DicomMap& result, - const char* dicom, - size_t size) + bool DicomMap::IsDicomFile(const char* dicom, + size_t size) { /** * http://dicom.nema.org/medical/dicom/current/output/chtml/part10/chapter_7.html @@ -756,11 +755,19 @@ * account to determine whether the file is or is not a DICOM file. **/ - if (size < 132 || - dicom[128] != 'D' || - dicom[129] != 'I' || - dicom[130] != 'C' || - dicom[131] != 'M') + return (size >= 132 && + dicom[128] == 'D' && + dicom[129] == 'I' && + dicom[130] == 'C' && + dicom[131] == 'M'); + } + + + bool DicomMap::ParseDicomMetaInformation(DicomMap& result, + const char* dicom, + size_t size) + { + if (!IsDicomFile(dicom, size)) { return false; } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomFormat/DicomMap.h --- a/Core/DicomFormat/DicomMap.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomFormat/DicomMap.h Wed Apr 01 10:15:33 2020 +0200 @@ -180,6 +180,9 @@ void GetTags(std::set& tags) const; + static bool IsDicomFile(const char* dicom, + size_t size); + static bool ParseDicomMetaInformation(DicomMap& result, const char* dicom, size_t size); diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/DicomServer.cpp --- a/Core/DicomNetworking/DicomServer.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -94,6 +94,7 @@ moveRequestHandlerFactory_ = NULL; storeRequestHandlerFactory_ = NULL; worklistRequestHandlerFactory_ = NULL; + storageCommitmentFactory_ = NULL; applicationEntityFilter_ = NULL; checkCalledAet_ = true; associationTimeout_ = 30; @@ -289,6 +290,29 @@ } } + void DicomServer::SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& factory) + { + Stop(); + storageCommitmentFactory_ = &factory; + } + + bool DicomServer::HasStorageCommitmentRequestHandlerFactory() const + { + return (storageCommitmentFactory_ != NULL); + } + + IStorageCommitmentRequestHandlerFactory& DicomServer::GetStorageCommitmentRequestHandlerFactory() const + { + if (HasStorageCommitmentRequestHandlerFactory()) + { + return *storageCommitmentFactory_; + } + else + { + throw OrthancException(ErrorCode_NoStorageCommitmentHandler); + } + } + void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory) { Stop(); @@ -378,5 +402,4 @@ return modalities_->IsSameAETitle(aet, GetApplicationEntityTitle()); } } - } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/DicomServer.h --- a/Core/DicomNetworking/DicomServer.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.h Wed Apr 01 10:15:33 2020 +0200 @@ -41,6 +41,7 @@ #include "IMoveRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" #include "IWorklistRequestHandlerFactory.h" +#include "IStorageCommitmentRequestHandlerFactory.h" #include "IApplicationEntityFilter.h" #include "RemoteModalityParameters.h" @@ -82,6 +83,7 @@ IMoveRequestHandlerFactory* moveRequestHandlerFactory_; IStoreRequestHandlerFactory* storeRequestHandlerFactory_; IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; + IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_; IApplicationEntityFilter* applicationEntityFilter_; static void ServerThread(DicomServer* server); @@ -122,6 +124,10 @@ bool HasWorklistRequestHandlerFactory() const; IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const; + void SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& handler); + bool HasStorageCommitmentRequestHandlerFactory() const; + IStorageCommitmentRequestHandlerFactory& GetStorageCommitmentRequestHandlerFactory() const; + void SetApplicationEntityFilter(IApplicationEntityFilter& handler); bool HasApplicationEntityFilter() const; IApplicationEntityFilter& GetApplicationEntityFilter() const; diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/DicomUserConnection.cpp --- a/Core/DicomNetworking/DicomUserConnection.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomUserConnection.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -86,6 +86,7 @@ # error The macro DCMTK_VERSION_NUMBER must be defined #endif +#include "../Compatibility.h" #include "../DicomFormat/DicomArray.h" #include "../Logging.h" #include "../OrthancException.h" @@ -158,7 +159,9 @@ void CheckIsOpen() const; - void Store(DcmInputStream& is, + void Store(std::string& sopClassUidOut /* out */, + std::string& sopInstanceUidOut /* out */, + DcmInputStream& is, DicomUserConnection& connection, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); @@ -252,7 +255,8 @@ } - void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax) + void DicomUserConnection::SetupPresentationContexts(Mode mode, + const std::string& preferredTransferSyntax) { // Flatten an array with the preferred transfer syntax const char* asPreferred[1] = { preferredTransferSyntax.c_str() }; @@ -274,30 +278,73 @@ } CheckStorageSOPClassesInvariant(); - unsigned int presentationContextId = 1; + + 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::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; + } - for (std::set::const_iterator it = storageSOPClasses_.begin(); - it != storageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } + case Mode_RequestStorageCommitment: + case Mode_ReportStorageCommitment: + { + const char* as = UID_StorageCommitmentPushModelSOPClass; + + std::vector ts; + ts.push_back(UID_LittleEndianExplicitTransferSyntax); + ts.push_back(UID_LittleEndianImplicitTransferSyntax); - for (std::set::const_iterator it = defaultStorageSOPClasses_.begin(); - it != defaultStorageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); + 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) { @@ -307,7 +354,9 @@ } - void DicomUserConnection::PImpl::Store(DcmInputStream& is, + void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut, + std::string& sopInstanceUidOut, + DcmInputStream& is, DicomUserConnection& connection, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) @@ -390,6 +439,9 @@ 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) @@ -1060,7 +1112,7 @@ } } - void DicomUserConnection::Open() + void DicomUserConnection::OpenInternal(Mode mode) { if (IsOpen()) { @@ -1100,7 +1152,7 @@ Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false), remoteAet_, "connecting"); - SetupPresentationContexts(preferredTransferSyntax_); + SetupPresentationContexts(mode, preferredTransferSyntax_); // Do the association Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_), @@ -1144,7 +1196,9 @@ return pimpl_->IsOpen(); } - void DicomUserConnection::Store(const char* buffer, + void DicomUserConnection::Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) @@ -1155,26 +1209,31 @@ is.setBuffer(buffer, size); is.setEos(); - pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); + pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); } - void DicomUserConnection::Store(const std::string& buffer, + 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(&buffer[0], buffer.size(), moveOriginatorAET, moveOriginatorID); + Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(), + moveOriginatorAET, moveOriginatorID); else - Store(NULL, 0, moveOriginatorAET, moveOriginatorID); + Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID); } - void DicomUserConnection::StoreFile(const std::string& path, + 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(is, *this, moveOriginatorAET, moveOriginatorID); + pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); } bool DicomUserConnection::Echo() @@ -1405,4 +1464,369 @@ 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 c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/DicomUserConnection.h --- a/Core/DicomNetworking/DicomUserConnection.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/DicomUserConnection.h Wed Apr 01 10:15:33 2020 +0200 @@ -54,6 +54,13 @@ struct PImpl; boost::shared_ptr pimpl_; + enum Mode + { + Mode_Generic, + Mode_ReportStorageCommitment, + Mode_RequestStorageCommitment + }; + // Connection parameters std::string preferredTransferSyntax_; std::string localAet_; @@ -67,7 +74,8 @@ void CheckIsOpen() const; - void SetupPresentationContexts(const std::string& preferredTransferSyntax); + void SetupPresentationContexts(Mode mode, + const std::string& preferredTransferSyntax); void MoveInternal(const std::string& targetAet, ResourceType level, @@ -79,6 +87,8 @@ void DefaultSetup(); + void OpenInternal(Mode mode); + public: DicomUserConnection(); @@ -137,7 +147,10 @@ void AddStorageSOPClass(const char* sop); - void Open(); + void Open() + { + OpenInternal(Mode_Generic); + } void Close(); @@ -145,33 +158,45 @@ bool Echo(); - void Store(const char* buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void Store(const char* buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const char* buffer, size_t size) { - Store(buffer, size, "", 0); // Not a C-Move + Store(sopClassUid, sopInstanceUid, buffer, size, "", 0); // Not a C-Move } - void Store(const std::string& buffer, + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void Store(const std::string& buffer) + void Store(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& buffer) { - Store(buffer, "", 0); // Not a C-Move + Store(sopClassUid, sopInstanceUid, buffer, "", 0); // Not a C-Move } - void StoreFile(const std::string& path, + void StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); - void StoreFile(const std::string& path) + void StoreFile(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + const std::string& path) { - StoreFile(path, "", 0); // Not a C-Move + StoreFile(sopClassUid, sopInstanceUid, path, "", 0); // Not a C-Move } void Find(DicomFindAnswers& result, @@ -212,5 +237,17 @@ 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 c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/IStorageCommitmentRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,66 @@ +/** + * 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 IStorageCommitmentRequestHandler : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentRequestHandler() + { + } + + virtual void HandleRequest(const std::string& transactionUid, + const std::vector& sopClassUids, + const std::vector& sopInstanceUids, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + + virtual void HandleReport(const std::string& transactionUid, + const std::vector& successSopClassUids, + const std::vector& successSopInstanceUids, + const std::vector& failedSopClassUids, + const std::vector& failedSopInstanceUids, + const std::vector& failureReasons, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + }; +} diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h Wed Apr 01 10:15:33 2020 +0200 @@ -0,0 +1,49 @@ +/** + * 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 "IStorageCommitmentRequestHandler.h" + +namespace Orthanc +{ + class IStorageCommitmentRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IStorageCommitmentRequestHandlerFactory() + { + } + + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() = 0; + }; +} diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -93,9 +93,12 @@ #include "../../Compatibility.h" #include "../../Toolbox.h" #include "../../Logging.h" +#include "../../OrthancException.h" +#include /* for storage commitment */ +#include /* for class DcmSequenceOfItems */ +#include /* for variable dcmAllStorageSOPClassUIDs */ #include /* for class DcmAssociationConfiguration */ -#include /* for variable dcmAllStorageSOPClassUIDs */ #include @@ -272,33 +275,6 @@ OFString sprofile; OFString temp_str; - std::vector knownAbstractSyntaxes; - - // For C-STORE - if (server.HasStoreRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_VerificationSOPClass); - } - - // For C-FIND - if (server.HasFindRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); - knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); - } - - if (server.HasWorklistRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); - } - - // For C-MOVE - if (server.HasMoveRequestHandlerFactory()) - { - knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); - knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); - } - cond = ASC_receiveAssociation(net, &assoc, /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, NULL, NULL, @@ -362,146 +338,206 @@ << " on IP " << remoteIp; - std::vector transferSyntaxes; - - // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1 - transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); - transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); - transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); - - // New transfer syntaxes supported since Orthanc 0.7.2 - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated)) { - transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); - } + /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE, + and storage commitment, if presented */ - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg)) - { - transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax); - transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax); - } + std::vector genericTransferSyntaxes; + genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000)) - { - transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax); - transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax); - } + std::vector knownAbstractSyntaxes; - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless)) - { - transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax); - transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax); - } - - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) - { - transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); - transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); - } + // For C-ECHO (always enabled since Orthanc 1.6.0; in earlier + // versions, only enabled if C-STORE was also enabled) + knownAbstractSyntaxes.push_back(UID_VerificationSOPClass); - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2)) - { - transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); - transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); - } + // For C-FIND + if (server.HasFindRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); + } -#if DCMTK_VERSION_NUMBER >= 361 - // New in Orthanc 1.6.0 - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4)) - { - transferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax); - transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax); - transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax); - transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax); - transferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax); - } -#endif - - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) - { - transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); - } + if (server.HasWorklistRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); + } - /* accept the Verification SOP Class if presented */ - cond = ASC_acceptContextsWithPreferredTransferSyntaxes( - assoc->params, - &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), - &transferSyntaxes[0], transferSyntaxes.size()); - if (cond.bad()) - { - LOG(INFO) << cond.text(); - AssociationCleanup(assoc); - return NULL; - } - - /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */ - size_t count = 0; - while (dcmAllStorageSOPClassUIDs[count] != NULL) - { - count++; - } + // For C-MOVE + if (server.HasMoveRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); + } -#if DCMTK_VERSION_NUMBER >= 362 - // The global variable "numberOfDcmAllStorageSOPClassUIDs" is - // only published if DCMTK >= 3.6.2: - // https://bitbucket.org/sjodogne/orthanc/issues/137 - assert(static_cast(count) == numberOfDcmAllStorageSOPClassUIDs); -#endif - - cond = ASC_acceptContextsWithPreferredTransferSyntaxes( - assoc->params, - dcmAllStorageSOPClassUIDs, count, - &transferSyntaxes[0], transferSyntaxes.size()); - if (cond.bad()) - { - LOG(INFO) << cond.text(); - AssociationCleanup(assoc); - return NULL; - } - - if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) - { - /* - * Promiscous mode is enabled: Accept everything not known not - * to be a storage SOP class. - **/ - cond = acceptUnknownContextsWithPreferredTransferSyntaxes( - assoc->params, &transferSyntaxes[0], transferSyntaxes.size(), ASC_SC_ROLE_DEFAULT); + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( + assoc->params, + &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(), + &genericTransferSyntaxes[0], genericTransferSyntaxes.size()); if (cond.bad()) { LOG(INFO) << cond.text(); AssociationCleanup(assoc); return NULL; } + + + /* storage commitment support, new in Orthanc 1.6.0 */ + if (server.HasStorageCommitmentRequestHandlerFactory()) + { + /** + * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept + * remote storage commitment requests, and the "SCP" role is + * needed to receive storage commitments answers. + **/ + const char* as[1] = { UID_StorageCommitmentPushModelSOPClass }; + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( + assoc->params, as, 1, + &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + } + } + + + { + /* accept the abstract syntaxes for C-STORE, if presented */ + + std::vector storageTransferSyntaxes; + + // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1 + storageTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + storageTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax); + storageTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax); + + // New transfer syntaxes supported since Orthanc 0.7.2 + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated)) + { + storageTransferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg)) + { + storageTransferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000)) + { + storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless)) + { + storageTransferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) + { + storageTransferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); + storageTransferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2)) + { + storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); + } + +#if DCMTK_VERSION_NUMBER >= 361 + // New in Orthanc 1.6.0 + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4)) + { + storageTransferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax); + storageTransferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax); + } +#endif + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) + { + storageTransferSyntaxes.push_back(UID_RLELosslessTransferSyntax); + } + + /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */ + size_t count = 0; + while (dcmAllStorageSOPClassUIDs[count] != NULL) + { + count++; + } + +#if DCMTK_VERSION_NUMBER >= 362 + // The global variable "numberOfDcmAllStorageSOPClassUIDs" is + // only published if DCMTK >= 3.6.2: + // https://bitbucket.org/sjodogne/orthanc/issues/137 + assert(static_cast(count) == numberOfDcmAllStorageSOPClassUIDs); +#endif + + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( + assoc->params, + dcmAllStorageSOPClassUIDs, count, + &storageTransferSyntaxes[0], storageTransferSyntaxes.size()); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) + { + /* + * Promiscous mode is enabled: Accept everything not known not + * to be a storage SOP class. + **/ + cond = acceptUnknownContextsWithPreferredTransferSyntaxes( + assoc->params, &storageTransferSyntaxes[0], storageTransferSyntaxes.size(), ASC_SC_ROLE_DEFAULT); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + } } /* set our app title */ @@ -703,6 +739,16 @@ supported = true; break; + case DIMSE_N_ACTION_RQ: + request = DicomRequestType_NAction; + supported = true; + break; + + case DIMSE_N_EVENT_REPORT_RQ: + request = DicomRequestType_NEventReport; + supported = true; + break; + default: // we cannot handle this kind of message cond = DIMSE_BADCOMMANDTYPE; @@ -784,6 +830,14 @@ } break; + case DicomRequestType_NAction: + cond = NActionScp(&msg, presID); + break; + + case DicomRequestType_NEventReport: + cond = NEventReportScp(&msg, presID); + break; + default: // Should never happen break; @@ -837,5 +891,379 @@ } return cond; } + + + static DcmDataset* ReadDataset(T_ASC_Association* assoc, + const char* errorMessage, + int timeout) + { + DcmDataset *tmp = NULL; + T_ASC_PresentationContextID presIdData; + + OFCondition cond = DIMSE_receiveDataSetInMemory( + assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, + &presIdData, &tmp, NULL, NULL); + if (!cond.good() || + tmp == NULL) + { + throw OrthancException(ErrorCode_NetworkProtocol, errorMessage); + } + + return tmp; + } + + + static std::string ReadString(DcmDataset& dataset, + const DcmTagKey& tag) + { + const char* s = NULL; + if (!dataset.findAndGetString(tag, s).good() || + s == NULL) + { + char buf[64]; + sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)", + tag.getGroup(), tag.getElement()); + throw OrthancException(ErrorCode_NetworkProtocol, buf); + } + + return std::string(s); + } + + + static void ReadSopSequence( + std::vector& sopClassUids, + std::vector& sopInstanceUids, + std::vector* failureReasons, // Can be NULL + DcmDataset& dataset, + const DcmTagKey& tag, + bool mandatory) + { + sopClassUids.clear(); + sopInstanceUids.clear(); + + if (failureReasons) + { + failureReasons->clear(); + } + + DcmSequenceOfItems* sequence = NULL; + if (!dataset.findAndGetSequence(tag, sequence).good() || + sequence == NULL) + { + if (mandatory) + { + char buf[64]; + sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)", + tag.getGroup(), tag.getElement()); + throw OrthancException(ErrorCode_NetworkProtocol, buf); + } + else + { + return; + } + } + + sopClassUids.reserve(sequence->card()); + sopInstanceUids.reserve(sequence->card()); + + if (failureReasons) + { + failureReasons->reserve(sequence->card()); + } + + for (unsigned long i = 0; i < sequence->card(); i++) + { + const char* a = NULL; + const char* b = NULL; + if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() || + !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() || + a == NULL || + b == NULL) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Missing Referenced SOP Class/Instance UID " + "in storage commitment dataset"); + } + + sopClassUids.push_back(a); + sopInstanceUids.push_back(b); + + if (failureReasons != NULL) + { + Uint16 reason; + if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Missing Failure Reason (0008,1197) " + "in storage commitment dataset"); + } + + failureReasons->push_back(static_cast(reason)); + } + } + } + + + OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID) + { + /** + * Starting with Orthanc 1.6.0, only storage commitment is + * supported with DICOM N-ACTION. This corresponds to the case + * where "Action Type ID" equals "1". + * 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 + **/ + + if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ || + !server_.HasStorageCommitmentRequestHandlerFactory()) + { + throw OrthancException(ErrorCode_InternalError); + } + + + /** + * Check that the storage commitment request is correctly formatted. + **/ + + const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ; + + if (request.ActionTypeID != 1) + { + throw OrthancException(ErrorCode_NotImplemented, + "Only storage commitment is implemented for DICOM N-ACTION SCP"); + } + + if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Unexpected incoming SOP class or instance UID for storage commitment"); + } + + if (request.DataSetType != DIMSE_DATASET_PRESENT) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Incoming storage commitment request without a dataset"); + } + + + /** + * Extract the DICOM dataset that is associated with the DIMSE + * message. The content of this dataset is documented in "Table + * J.3-1. Storage Commitment Request - Action Information": + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1 + **/ + + std::unique_ptr dataset( + ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_)); + + std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); + + std::vector sopClassUid, sopInstanceUid; + ReadSopSequence(sopClassUid, sopInstanceUid, NULL, + *dataset, DCM_ReferencedSOPSequence, true /* mandatory */); + + LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid; + + for (size_t i = 0; i < sopClassUid.size(); i++) + { + LOG(INFO) << " (" << (i + 1) << "/" << sopClassUid.size() + << ") queried SOP Class/Instance UID: " + << sopClassUid[i] << " / " << sopInstanceUid[i]; + } + + + /** + * Call the Orthanc handler. The list of available DIMSE status + * codes can be found at: + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10 + **/ + + DIC_US dimseStatus; + + try + { + std::unique_ptr handler + (server_.GetStorageCommitmentRequestHandlerFactory(). + ConstructStorageCommitmentRequestHandler()); + + handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid, + remoteIp_, remoteAet_, calledAet_); + + dimseStatus = 0; // Success + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error while processing an incoming storage commitment request: " << e.What(); + + // Code 0x0110 - "General failure in processing the operation was encountered" + dimseStatus = STATUS_N_ProcessingFailure; + } + + + /** + * Send the DIMSE status back to the SCU. + **/ + + { + T_DIMSE_Message response; + memset(&response, 0, sizeof(response)); + response.CommandField = DIMSE_N_ACTION_RSP; + + T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP; + content.MessageIDBeingRespondedTo = request.MessageID; + strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + content.DimseStatus = dimseStatus; + strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts" + content.DataSetType = DIMSE_DATASET_NULL; // Dataset is absent in storage commitment response + content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID; + + return DIMSE_sendMessageUsingMemoryData( + assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */, + NULL /* callback */, NULL /* callback context */, NULL /* commandSet */); + } + } + + + OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID) + { + /** + * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for + * storage commitment. + * 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 + **/ + + if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ || + !server_.HasStorageCommitmentRequestHandlerFactory()) + { + throw OrthancException(ErrorCode_InternalError); + } + + + /** + * Check that the storage commitment report is correctly formatted. + **/ + + const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ; + + if (report.EventTypeID != 1 /* successful */ && + report.EventTypeID != 2 /* failures exist */) + { + throw OrthancException(ErrorCode_NotImplemented, + "Unknown event for DICOM N-EVENT-REPORT SCP"); + } + + if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Unexpected incoming SOP class or instance UID for storage commitment"); + } + + if (report.DataSetType != DIMSE_DATASET_PRESENT) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "Incoming storage commitment report without a dataset"); + } + + + /** + * Extract the DICOM dataset that is associated with the DIMSE + * message. The content of this dataset is documented in "Table + * J.3-2. Storage Commitment Result - Event Information": + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2 + **/ + + std::unique_ptr dataset( + ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_)); + + std::string transactionUid = ReadString(*dataset, DCM_TransactionUID); + + std::vector successSopClassUid, successSopInstanceUid; + ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL, + *dataset, DCM_ReferencedSOPSequence, + (report.EventTypeID == 1) /* mandatory in the case of success */); + + std::vector failedSopClassUid, failedSopInstanceUid; + std::vector failureReasons; + + if (report.EventTypeID == 2 /* failures exist */) + { + ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons, + *dataset, DCM_FailedSOPSequence, true); + } + + LOG(INFO) << "Incoming storage commitment report, with transaction UID: " << transactionUid; + + for (size_t i = 0; i < successSopClassUid.size(); i++) + { + LOG(INFO) << " (success " << (i + 1) << "/" << successSopClassUid.size() + << ") SOP Class/Instance UID: " + << successSopClassUid[i] << " / " << successSopInstanceUid[i]; + } + + for (size_t i = 0; i < failedSopClassUid.size(); i++) + { + LOG(INFO) << " (failure " << (i + 1) << "/" << failedSopClassUid.size() + << ") SOP Class/Instance UID: " + << failedSopClassUid[i] << " / " << failedSopInstanceUid[i]; + } + + /** + * Call the Orthanc handler. The list of available DIMSE status + * codes can be found at: + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10 + **/ + + DIC_US dimseStatus; + + try + { + std::unique_ptr handler + (server_.GetStorageCommitmentRequestHandlerFactory(). + ConstructStorageCommitmentRequestHandler()); + + handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid, + failedSopClassUid, failedSopInstanceUid, failureReasons, + remoteIp_, remoteAet_, calledAet_); + + dimseStatus = 0; // Success + } + catch (OrthancException& e) + { + LOG(ERROR) << "Error while processing an incoming storage commitment report: " << e.What(); + + // Code 0x0110 - "General failure in processing the operation was encountered" + dimseStatus = STATUS_N_ProcessingFailure; + } + + + /** + * Send the DIMSE status back to the SCU. + **/ + + { + T_DIMSE_Message response; + memset(&response, 0, sizeof(response)); + response.CommandField = DIMSE_N_EVENT_REPORT_RSP; + + T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP; + content.MessageIDBeingRespondedTo = report.MessageID; + strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + content.DimseStatus = dimseStatus; + strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts" + content.DataSetType = DIMSE_DATASET_NULL; // Dataset is absent in storage commitment response + content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID; + + return DIMSE_sendMessageUsingMemoryData( + assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */, + NULL /* callback */, NULL /* callback context */, NULL /* commandSet */); + } + } } } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/Internals/CommandDispatcher.h --- a/Core/DicomNetworking/Internals/CommandDispatcher.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.h Wed Apr 01 10:15:33 2020 +0200 @@ -56,6 +56,12 @@ std::string calledAet_; IApplicationEntityFilter* filter_; + OFCondition NActionScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID); + + OFCondition NEventReportScp(T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID); + public: CommandDispatcher(const DicomServer& server, T_ASC_Association* assoc, @@ -69,11 +75,11 @@ virtual bool Step(); }; - OFCondition EchoScp(T_ASC_Association * assoc, - T_DIMSE_Message * msg, - T_ASC_PresentationContextID presID); - CommandDispatcher* AcceptAssociation(const DicomServer& server, T_ASC_Network *net); + + OFCondition EchoScp(T_ASC_Association* assoc, + T_DIMSE_Message* msg, + T_ASC_PresentationContextID presID); } } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/RemoteModalityParameters.cpp --- a/Core/DicomNetworking/RemoteModalityParameters.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -48,6 +48,9 @@ static const char* KEY_ALLOW_GET = "AllowGet"; static const char* KEY_ALLOW_MOVE = "AllowMove"; static const char* KEY_ALLOW_STORE = "AllowStore"; +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_HOST = "Host"; static const char* KEY_MANUFACTURER = "Manufacturer"; static const char* KEY_PORT = "Port"; @@ -66,6 +69,8 @@ allowFind_ = true; allowMove_ = true; allowGet_ = true; + allowNAction_ = true; // For storage commitment + allowNEventReport_ = true; // For storage commitment } @@ -211,6 +216,23 @@ { allowMove_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_MOVE); } + + if (serialized.isMember(KEY_ALLOW_N_ACTION)) + { + allowNAction_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_ACTION); + } + + if (serialized.isMember(KEY_ALLOW_N_EVENT_REPORT)) + { + allowNEventReport_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_EVENT_REPORT); + } + + if (serialized.isMember(KEY_ALLOW_STORAGE_COMMITMENT)) + { + bool allow = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORAGE_COMMITMENT); + allowNAction_ = allow; + allowNEventReport_ = allow; + } } @@ -233,6 +255,12 @@ case DicomRequestType_Store: return allowStore_; + case DicomRequestType_NAction: + return allowNAction_; + + case DicomRequestType_NEventReport: + return allowNEventReport_; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -264,6 +292,14 @@ allowStore_ = allowed; break; + case DicomRequestType_NAction: + allowNAction_ = allowed; + break; + + case DicomRequestType_NEventReport: + allowNEventReport_ = allowed; + break; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -276,7 +312,9 @@ !allowStore_ || !allowFind_ || !allowGet_ || - !allowMove_); + !allowMove_ || + !allowNAction_ || + !allowNEventReport_); } @@ -296,6 +334,8 @@ target[KEY_ALLOW_FIND] = allowFind_; target[KEY_ALLOW_GET] = allowGet_; target[KEY_ALLOW_MOVE] = allowMove_; + target[KEY_ALLOW_N_ACTION] = allowNAction_; + target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_; } else { diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomNetworking/RemoteModalityParameters.h --- a/Core/DicomNetworking/RemoteModalityParameters.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Wed Apr 01 10:15:33 2020 +0200 @@ -53,7 +53,9 @@ bool allowFind_; bool allowMove_; bool allowGet_; - + bool allowNAction_; + bool allowNEventReport_; + void Clear(); void UnserializeArray(const Json::Value& serialized); diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -68,10 +68,11 @@ #include #include #include +#include #include #include #include -#include +#include #include #include @@ -1878,7 +1879,15 @@ std::unique_ptr result(new DcmFileFormat); result->transferInit(); - if (!result->read(is).good()) + + /** + * New in Orthanc 1.6.0: The "size" is given as an argument to the + * "read()" method. This can avoid huge memory consumption if + * parsing an invalid DICOM file, which can notably been observed + * by executing the integration test "test_upload_compressed" on + * valgrind running Orthanc. + **/ + if (!result->read(is, EXS_Unknown, EGL_noChange, size).good()) { throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse an invalid DICOM file (size: " + @@ -2081,7 +2090,7 @@ // Unregister JPEG codecs DJDecoderRegistration::cleanup(); # if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 - DJDecoderRegistration::cleanup(); + DJEncoderRegistration::cleanup(); # endif #endif } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/FromDcmtkBridge.h --- a/Core/DicomParsing/FromDcmtkBridge.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.h Wed Apr 01 10:15:33 2020 +0200 @@ -271,7 +271,10 @@ ITagVisitor& visitor, Encoding defaultEncoding); - static bool GetDcmtkTransferSyntax(E_TransferSyntax& target, - DicomTransferSyntax syntax); + static bool LookupDcmtkTransferSyntax(E_TransferSyntax& target, + DicomTransferSyntax source); + + static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, + E_TransferSyntax source); }; } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h --- a/Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h Wed Apr 01 10:15:33 2020 +0200 @@ -34,10 +34,10 @@ namespace Orthanc { - bool GetDcmtkTransferSyntax(E_TransferSyntax& target, - DicomTransferSyntax syntax) + bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target, + DicomTransferSyntax source) { - switch (syntax) + switch (source) { case DicomTransferSyntax_LittleEndianImplicit: target = EXS_LittleEndianImplicit; @@ -56,75 +56,147 @@ return true; case DicomTransferSyntax_JPEGProcess1: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess1TransferSyntax; +# else target = EXS_JPEGProcess1; +# endif return true; case DicomTransferSyntax_JPEGProcess2_4: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess2_4TransferSyntax; +# else target = EXS_JPEGProcess2_4; +# endif return true; case DicomTransferSyntax_JPEGProcess3_5: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess3_5TransferSyntax; +# else target = EXS_JPEGProcess3_5; +# endif return true; case DicomTransferSyntax_JPEGProcess6_8: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess6_8TransferSyntax; +# else target = EXS_JPEGProcess6_8; +# endif return true; case DicomTransferSyntax_JPEGProcess7_9: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess7_9TransferSyntax; +# else target = EXS_JPEGProcess7_9; +# endif return true; case DicomTransferSyntax_JPEGProcess10_12: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess10_12TransferSyntax; +# else target = EXS_JPEGProcess10_12; +# endif return true; case DicomTransferSyntax_JPEGProcess11_13: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess11_13TransferSyntax; +# else target = EXS_JPEGProcess11_13; +# endif return true; case DicomTransferSyntax_JPEGProcess14: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess14TransferSyntax; +# else target = EXS_JPEGProcess14; +# endif return true; case DicomTransferSyntax_JPEGProcess15: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess15TransferSyntax; +# else target = EXS_JPEGProcess15; +# endif return true; case DicomTransferSyntax_JPEGProcess16_18: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess16_18TransferSyntax; +# else target = EXS_JPEGProcess16_18; +# endif return true; case DicomTransferSyntax_JPEGProcess17_19: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess17_19TransferSyntax; +# else target = EXS_JPEGProcess17_19; +# endif return true; case DicomTransferSyntax_JPEGProcess20_22: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess20_22TransferSyntax; +# else target = EXS_JPEGProcess20_22; +# endif return true; case DicomTransferSyntax_JPEGProcess21_23: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess21_23TransferSyntax; +# else target = EXS_JPEGProcess21_23; +# endif return true; case DicomTransferSyntax_JPEGProcess24_26: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess24_26TransferSyntax; +# else target = EXS_JPEGProcess24_26; +# endif return true; case DicomTransferSyntax_JPEGProcess25_27: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess25_27TransferSyntax; +# else target = EXS_JPEGProcess25_27; +# endif return true; case DicomTransferSyntax_JPEGProcess28: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess28TransferSyntax; +# else target = EXS_JPEGProcess28; +# endif return true; case DicomTransferSyntax_JPEGProcess29: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess29TransferSyntax; +# else target = EXS_JPEGProcess29; +# endif return true; case DicomTransferSyntax_JPEGProcess14SV1: +# if DCMTK_VERSION_NUMBER <= 360 + target = EXS_JPEGProcess14SV1TransferSyntax; +# else target = EXS_JPEGProcess14SV1; +# endif return true; case DicomTransferSyntax_JPEGLSLossless: @@ -167,25 +239,35 @@ target = EXS_MPEG2MainProfileAtHighLevel; return true; +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4HighProfileLevel4_1: target = EXS_MPEG4HighProfileLevel4_1; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1: target = EXS_MPEG4BDcompatibleHighProfileLevel4_1; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo: target = EXS_MPEG4HighProfileLevel4_2_For2DVideo; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo: target = EXS_MPEG4HighProfileLevel4_2_For3DVideo; return true; +#endif +#if DCMTK_VERSION_NUMBER >= 361 case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2: target = EXS_MPEG4StereoHighProfileLevel4_2; return true; +#endif #if DCMTK_VERSION_NUMBER >= 362 case DicomTransferSyntax_HEVCMainProfileLevel5_1: @@ -207,4 +289,261 @@ return false; } } + + + bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target, + E_TransferSyntax source) + { + switch (source) + { + case EXS_LittleEndianImplicit: + target = DicomTransferSyntax_LittleEndianImplicit; + return true; + + case EXS_LittleEndianExplicit: + target = DicomTransferSyntax_LittleEndianExplicit; + return true; + + case EXS_DeflatedLittleEndianExplicit: + target = DicomTransferSyntax_DeflatedLittleEndianExplicit; + return true; + + case EXS_BigEndianExplicit: + target = DicomTransferSyntax_BigEndianExplicit; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess1TransferSyntax: +# else + case EXS_JPEGProcess1: +# endif + target = DicomTransferSyntax_JPEGProcess1; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess2_4TransferSyntax: +# else + case EXS_JPEGProcess2_4: +# endif + target = DicomTransferSyntax_JPEGProcess2_4; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess3_5TransferSyntax: +# else + case EXS_JPEGProcess3_5: +# endif + target = DicomTransferSyntax_JPEGProcess3_5; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess6_8TransferSyntax: +# else + case EXS_JPEGProcess6_8: +# endif + target = DicomTransferSyntax_JPEGProcess6_8; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess7_9TransferSyntax: +# else + case EXS_JPEGProcess7_9: +# endif + target = DicomTransferSyntax_JPEGProcess7_9; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess10_12TransferSyntax: +# else + case EXS_JPEGProcess10_12: +# endif + target = DicomTransferSyntax_JPEGProcess10_12; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess11_13TransferSyntax: +# else + case EXS_JPEGProcess11_13: +# endif + target = DicomTransferSyntax_JPEGProcess11_13; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess14TransferSyntax: +# else + case EXS_JPEGProcess14: +# endif + target = DicomTransferSyntax_JPEGProcess14; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess15TransferSyntax: +# else + case EXS_JPEGProcess15: +# endif + target = DicomTransferSyntax_JPEGProcess15; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess16_18TransferSyntax: +# else + case EXS_JPEGProcess16_18: +# endif + target = DicomTransferSyntax_JPEGProcess16_18; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess17_19TransferSyntax: +# else + case EXS_JPEGProcess17_19: +# endif + target = DicomTransferSyntax_JPEGProcess17_19; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess20_22TransferSyntax: +# else + case EXS_JPEGProcess20_22: +# endif + target = DicomTransferSyntax_JPEGProcess20_22; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess21_23TransferSyntax: +# else + case EXS_JPEGProcess21_23: +# endif + target = DicomTransferSyntax_JPEGProcess21_23; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess24_26TransferSyntax: +# else + case EXS_JPEGProcess24_26: +# endif + target = DicomTransferSyntax_JPEGProcess24_26; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess25_27TransferSyntax: +# else + case EXS_JPEGProcess25_27: +# endif + target = DicomTransferSyntax_JPEGProcess25_27; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess28TransferSyntax: +# else + case EXS_JPEGProcess28: +# endif + target = DicomTransferSyntax_JPEGProcess28; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess29TransferSyntax: +# else + case EXS_JPEGProcess29: +# endif + target = DicomTransferSyntax_JPEGProcess29; + return true; + +# if DCMTK_VERSION_NUMBER <= 360 + case EXS_JPEGProcess14SV1TransferSyntax: +# else + case EXS_JPEGProcess14SV1: +# endif + target = DicomTransferSyntax_JPEGProcess14SV1; + return true; + + case EXS_JPEGLSLossless: + target = DicomTransferSyntax_JPEGLSLossless; + return true; + + case EXS_JPEGLSLossy: + target = DicomTransferSyntax_JPEGLSLossy; + return true; + + case EXS_JPEG2000LosslessOnly: + target = DicomTransferSyntax_JPEG2000LosslessOnly; + return true; + + case EXS_JPEG2000: + target = DicomTransferSyntax_JPEG2000; + return true; + + case EXS_JPEG2000MulticomponentLosslessOnly: + target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly; + return true; + + case EXS_JPEG2000Multicomponent: + target = DicomTransferSyntax_JPEG2000Multicomponent; + return true; + + case EXS_JPIPReferenced: + target = DicomTransferSyntax_JPIPReferenced; + return true; + + case EXS_JPIPReferencedDeflate: + target = DicomTransferSyntax_JPIPReferencedDeflate; + return true; + + case EXS_MPEG2MainProfileAtMainLevel: + target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel; + return true; + + case EXS_MPEG2MainProfileAtHighLevel: + target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel; + return true; + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4HighProfileLevel4_1: + target = DicomTransferSyntax_MPEG4HighProfileLevel4_1; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4BDcompatibleHighProfileLevel4_1: + target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4HighProfileLevel4_2_For2DVideo: + target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4HighProfileLevel4_2_For3DVideo: + target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 361 + case EXS_MPEG4StereoHighProfileLevel4_2: + target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 362 + case EXS_HEVCMainProfileLevel5_1: + target = DicomTransferSyntax_HEVCMainProfileLevel5_1; + return true; +#endif + +#if DCMTK_VERSION_NUMBER >= 362 + case EXS_HEVCMain10ProfileLevel5_1: + target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1; + return true; +#endif + + case EXS_RLELossless: + target = DicomTransferSyntax_RLELossless; + return true; + + default: + return false; + } + } } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/Internals/DicomFrameIndex.cpp --- a/Core/DicomParsing/Internals/DicomFrameIndex.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomFrameIndex.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -68,7 +68,10 @@ uint32_t length = item->getLength(); if (length == 0) { - table.clear(); + // Degenerate case: Empty offset table means only one frame + // that overlaps all the fragments + table.resize(1); + table[0] = 0; return; } @@ -146,7 +149,6 @@ throw OrthancException(ErrorCode_BadFileFormat); } - // Loop over the fragments (ignoring the offset table). This is // an alternative, faster implementation to DCMTK's // "DcmCodec::determineStartFragment()". @@ -318,46 +320,10 @@ }; - - bool DicomFrameIndex::IsVideo(DcmFileFormat& dicom) + unsigned int DicomFrameIndex::GetFramesCount(DcmDataset& dicom) { - // Retrieve the transfer syntax from the DICOM header - const char* value = NULL; - if (!dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() || - value == NULL) - { - return false; - } - - const std::string transferSyntax(value); - - // Video standards supported in DICOM 2016a - // http://dicom.nema.org/medical/dicom/2016a/output/html/part05.html - if (transferSyntax == "1.2.840.10008.1.2.4.100" || // MPEG2 MP@ML option of ISO/IEC MPEG2 - transferSyntax == "1.2.840.10008.1.2.4.101" || // MPEG2 MP@HL option of ISO/IEC MPEG2 - transferSyntax == "1.2.840.10008.1.2.4.102" || // MPEG-4 AVC/H.264 High Profile / Level 4.1 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.103" || // MPEG-4 AVC/H.264 BD-compat High Profile / Level 4.1 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.104" || // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.105" || // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264 - transferSyntax == "1.2.840.10008.1.2.4.106") // MPEG-4 AVC/H.264 Stereo High Profile / Level 4.2 of the ITU-T H.264 - { - return true; - } - - return false; - } - - - unsigned int DicomFrameIndex::GetFramesCount(DcmFileFormat& dicom) - { - // Assume 1 frame for video transfer syntaxes - if (IsVideo(dicom)) - { - return 1; - } - const char* tmp = NULL; - if (!dicom.getDataset()->findAndGetString(DCM_NumberOfFrames, tmp).good() || + if (!dicom.findAndGetString(DCM_NumberOfFrames, tmp).good() || tmp == NULL) { return 1; @@ -378,12 +344,12 @@ } else { - return count; + return static_cast(count); } } - DicomFrameIndex::DicomFrameIndex(DcmFileFormat& dicom) + DicomFrameIndex::DicomFrameIndex(DcmDataset& dicom) { countFrames_ = GetFramesCount(dicom); if (countFrames_ == 0) @@ -392,10 +358,8 @@ return; } - DcmDataset& dataset = *dicom.getDataset(); - // Test whether this image is composed of a sequence of fragments - DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset); + DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dicom); if (pixelSequence != NULL) { index_.reset(new FragmentIndex(pixelSequence, countFrames_)); @@ -404,18 +368,18 @@ // Extract information about the image structure DicomMap tags; - FromDcmtkBridge::ExtractDicomSummary(tags, dataset); + FromDcmtkBridge::ExtractDicomSummary(tags, dicom); DicomImageInformation information(tags); // Access to the raw pixel data - if (DicomImageDecoder::IsPsmctRle1(dataset)) + if (DicomImageDecoder::IsPsmctRle1(dicom)) { - index_.reset(new PsmctRle1Index(dataset, countFrames_, information.GetFrameSize())); + index_.reset(new PsmctRle1Index(dicom, countFrames_, information.GetFrameSize())); } else { - index_.reset(new UncompressedIndex(dataset, countFrames_, information.GetFrameSize())); + index_.reset(new UncompressedIndex(dicom, countFrames_, information.GetFrameSize())); } } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/Internals/DicomFrameIndex.h --- a/Core/DicomParsing/Internals/DicomFrameIndex.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomFrameIndex.h Wed Apr 01 10:15:33 2020 +0200 @@ -67,7 +67,7 @@ unsigned int countFrames_; public: - DicomFrameIndex(DcmFileFormat& dicom); + DicomFrameIndex(DcmDataset& dicom); unsigned int GetFramesCount() const { @@ -77,8 +77,6 @@ void GetRawFrame(std::string& frame, unsigned int index) const; - static bool IsVideo(DcmFileFormat& dicom); - - static unsigned int GetFramesCount(DcmFileFormat& dicom); + static unsigned int GetFramesCount(DcmDataset& dicom); }; } diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/Internals/DicomImageDecoder.cpp --- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -34,6 +34,8 @@ #include "../../PrecompiledHeaders.h" #include "DicomImageDecoder.h" +#include "../ParsedDicomFile.h" + /*========================================================================= @@ -84,7 +86,6 @@ #include "../../DicomFormat/DicomIntegerPixelAccessor.h" #include "../ToDcmtkBridge.h" #include "../FromDcmtkBridge.h" -#include "../ParsedDicomFile.h" #if ORTHANC_ENABLE_PNG == 1 # include "../../Images/PngWriter.h" @@ -98,7 +99,6 @@ #include #include -#include #include #include #include @@ -662,7 +662,20 @@ ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom, unsigned int frame) { - DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + if (dicom.GetDcmtkObject().getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + return Decode(*dicom.GetDcmtkObject().getDataset(), frame); + } + } + + + ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset, + unsigned int frame) + { E_TransferSyntax syntax = dataset.getOriginalXfer(); /** diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/Internals/DicomImageDecoder.h --- a/Core/DicomParsing/Internals/DicomImageDecoder.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.h Wed Apr 01 10:15:33 2020 +0200 @@ -34,7 +34,7 @@ #pragma once #include "../../Compatibility.h" -#include "../ParsedDicomFile.h" +#include "../../Images/ImageAccessor.h" #include @@ -62,6 +62,8 @@ namespace Orthanc { + class ParsedDicomFile; + class DicomImageDecoder : public boost::noncopyable { private: @@ -102,6 +104,9 @@ static ImageAccessor *Decode(ParsedDicomFile& dicom, unsigned int frame); + static ImageAccessor *Decode(DcmDataset& dataset, + unsigned int frame); + static void ExtractPamImage(std::string& result, std::unique_ptr& image, ImageExtractionMode mode, diff -r c38b82bb6fd3 -r 320a2d224902 Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -1557,7 +1557,9 @@ { if (pimpl_->frameIndex_.get() == NULL) { - pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_)); + assert(pimpl_->file_ != NULL && + pimpl_->file_->getDataset() != NULL); + pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_->getDataset())); } pimpl_->frameIndex_->GetRawFrame(target, frameId); @@ -1589,7 +1591,9 @@ unsigned int ParsedDicomFile::GetFramesCount() const { - return DicomFrameIndex::GetFramesCount(*pimpl_->file_); + assert(pimpl_->file_ != NULL && + pimpl_->file_->getDataset() != NULL); + return DicomFrameIndex::GetFramesCount(*pimpl_->file_->getDataset()); } diff -r c38b82bb6fd3 -r 320a2d224902 Core/Enumerations.cpp --- a/Core/Enumerations.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Enumerations.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -366,6 +366,9 @@ case ErrorCode_AlreadyExistingTag: return "Cannot override the value of a tag that already exists"; + case ErrorCode_NoStorageCommitmentHandler: + return "No request handler factory for DICOM N-ACTION SCP (storage commitment)"; + case ErrorCode_UnsupportedMediaType: return "Unsupported media type"; @@ -860,6 +863,14 @@ return "Store"; break; + case DicomRequestType_NAction: + return "N-ACTION"; + break; + + case DicomRequestType_NEventReport: + return "N-EVENT-REPORT"; + break; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -1157,6 +1168,41 @@ } + const char* EnumerationToString(StorageCommitmentFailureReason reason) + { + switch (reason) + { + case StorageCommitmentFailureReason_Success: + return "Success"; + + case StorageCommitmentFailureReason_ProcessingFailure: + return "A general failure in processing the operation was encountered"; + + case StorageCommitmentFailureReason_NoSuchObjectInstance: + return "One or more of the elements in the Referenced SOP " + "Instance Sequence was not available"; + + case StorageCommitmentFailureReason_ResourceLimitation: + return "The SCP does not currently have enough resources to " + "store the requested SOP Instance(s)"; + + case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported: + return "Storage Commitment has been requested for a SOP Instance " + "with a SOP Class that is not supported by the SCP"; + + case StorageCommitmentFailureReason_ClassInstanceConflict: + return "The SOP Class of an element in the Referenced SOP Instance Sequence " + "did not correspond to the SOP class registered for this SOP Instance at the SCP"; + + case StorageCommitmentFailureReason_DuplicateTransactionUID: + return "The Transaction UID of the Storage Commitment Request is already in use"; + + default: + return "Unknown failure reason"; + } + } + + Encoding StringToEncoding(const char* encoding) { std::string s(encoding); diff -r c38b82bb6fd3 -r 320a2d224902 Core/Enumerations.h --- a/Core/Enumerations.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Enumerations.h Wed Apr 01 10:15:33 2020 +0200 @@ -239,6 +239,7 @@ ErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, ErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, ErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, + ErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, ErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, ErrorCode_START_PLUGINS = 1000000 }; @@ -670,7 +671,9 @@ DicomRequestType_Find, DicomRequestType_Get, DicomRequestType_Move, - DicomRequestType_Store + DicomRequestType_Store, + DicomRequestType_NAction, + DicomRequestType_NEventReport }; enum TransferSyntax @@ -712,6 +715,36 @@ JobStopReason_Retry }; + + // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 + enum StorageCommitmentFailureReason + { + StorageCommitmentFailureReason_Success = 0, + + // A general failure in processing the operation was encountered + StorageCommitmentFailureReason_ProcessingFailure = 0x0110, + + // One or more of the elements in the Referenced SOP Instance + // Sequence was not available + StorageCommitmentFailureReason_NoSuchObjectInstance = 0x0112, + + // The SCP does not currently have enough resources to store the + // requested SOP Instance(s) + StorageCommitmentFailureReason_ResourceLimitation = 0x0213, + + // Storage Commitment has been requested for a SOP Instance with a + // SOP Class that is not supported by the SCP + StorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 0x0122, + + // The SOP Class of an element in the Referenced SOP Instance + // Sequence did not correspond to the SOP class registered for + // this SOP Instance at the SCP + StorageCommitmentFailureReason_ClassInstanceConflict = 0x0119, + + // The Transaction UID of the Storage Commitment Request is already in use + StorageCommitmentFailureReason_DuplicateTransactionUID = 0x0131 + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -798,6 +831,8 @@ const char* EnumerationToString(Endianness endianness); + const char* EnumerationToString(StorageCommitmentFailureReason reason); + Encoding StringToEncoding(const char* encoding); ResourceType StringToResourceType(const char* type); diff -r c38b82bb6fd3 -r 320a2d224902 Core/HttpClient.cpp --- a/Core/HttpClient.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/HttpClient.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -234,7 +234,8 @@ if (sourceRemainingSize > 0) { // transmit the end of current source buffer - memcpy(curlBuffer + curlBufferFilledSize, sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize); + memcpy(curlBuffer + curlBufferFilledSize, + sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize); curlBufferFilledSize += sourceRemainingSize; } @@ -248,11 +249,13 @@ sourceRemainingSize = sourceBuffer_.size(); } - if (sourceRemainingSize > 0 && (curlBufferSize - curlBufferFilledSize) > 0) + if (sourceRemainingSize > 0 && + curlBufferSize > curlBufferFilledSize) { size_t s = std::min(sourceRemainingSize, curlBufferSize - curlBufferFilledSize); - memcpy(curlBuffer + curlBufferFilledSize, sourceBuffer_.data() + sourceBufferTransmittedSize_, s); + memcpy(curlBuffer + curlBufferFilledSize, + sourceBuffer_.data() + sourceBufferTransmittedSize_, s); sourceBufferTransmittedSize_ += s; curlBufferFilledSize += s; diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/IJob.h --- a/Core/JobsEngine/IJob.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/IJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -50,7 +50,7 @@ // Method called once the job enters the jobs engine virtual void Start() = 0; - virtual JobStepResult Step() = 0; + virtual JobStepResult Step(const std::string& jobId) = 0; // Method called once the job is resubmitted after a failure virtual void Reset() = 0; diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/JobsEngine.cpp --- a/Core/JobsEngine/JobsEngine.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/JobsEngine.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -71,7 +71,7 @@ try { - result = running.GetJob().Step(); + result = running.GetJob().Step(running.GetId()); } catch (OrthancException& e) { diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/JobsEngine.h --- a/Core/JobsEngine/JobsEngine.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/JobsEngine.h Wed Apr 01 10:15:33 2020 +0200 @@ -41,7 +41,7 @@ namespace Orthanc { - class JobsEngine + class JobsEngine : public boost::noncopyable { private: enum State diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp --- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -319,7 +319,7 @@ } - JobStepResult SequenceOfOperationsJob::Step() + JobStepResult SequenceOfOperationsJob::Step(const std::string& jobId) { boost::mutex::scoped_lock lock(mutex_); diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/Operations/SequenceOfOperationsJob.h --- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -126,30 +126,30 @@ size_t output); }; - virtual void Start() + virtual void Start() ORTHANC_OVERRIDE { } - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void Reset(); + virtual void Reset() ORTHANC_OVERRIDE; - virtual void Stop(JobStopReason reason); + virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE; - virtual float GetProgress(); + virtual float GetProgress() ORTHANC_OVERRIDE; - virtual void GetJobType(std::string& target) + virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE { target = "SequenceOfOperations"; } - virtual void GetPublicContent(Json::Value& value); + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& value); + virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime, - const std::string& key) + const std::string& key) ORTHANC_OVERRIDE { return false; } diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/SetOfCommandsJob.cpp --- a/Core/JobsEngine/SetOfCommandsJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/SetOfCommandsJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -145,7 +145,7 @@ } - JobStepResult SetOfCommandsJob::Step() + JobStepResult SetOfCommandsJob::Step(const std::string& jobId) { if (!started_) { @@ -169,7 +169,7 @@ try { // Not at the trailing step: Handle the current command - if (!commands_[position_]->Execute()) + if (!commands_[position_]->Execute(jobId)) { // Error if (!permissive_) diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/SetOfCommandsJob.h --- a/Core/JobsEngine/SetOfCommandsJob.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/SetOfCommandsJob.h Wed Apr 01 10:15:33 2020 +0200 @@ -49,7 +49,7 @@ { } - virtual bool Execute() = 0; + virtual bool Execute(const std::string& jobId) = 0; virtual void Serialize(Json::Value& target) const = 0; }; @@ -110,14 +110,14 @@ void SetPermissive(bool permissive); - virtual void Reset(); + virtual void Reset() ORTHANC_OVERRIDE; - virtual void Start() + virtual void Start() ORTHANC_OVERRIDE { started_ = true; } - virtual float GetProgress(); + virtual float GetProgress() ORTHANC_OVERRIDE; bool IsStarted() const { @@ -126,15 +126,15 @@ const ICommand& GetCommand(size_t index) const; - virtual JobStepResult Step(); + virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE; - virtual void GetPublicContent(Json::Value& value); + virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE; - virtual bool Serialize(Json::Value& target); + virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE; virtual bool GetOutput(std::string& output, MimeType& mime, - const std::string& key) + const std::string& key) ORTHANC_OVERRIDE { return false; } diff -r c38b82bb6fd3 -r 320a2d224902 Core/JobsEngine/SetOfInstancesJob.cpp --- a/Core/JobsEngine/SetOfInstancesJob.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/JobsEngine/SetOfInstancesJob.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -60,7 +60,7 @@ return instance_; } - virtual bool Execute() + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE { if (!that_.HandleInstance(instance_)) { @@ -73,7 +73,7 @@ } } - virtual void Serialize(Json::Value& target) const + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE { target = instance_; } @@ -91,12 +91,12 @@ { } - virtual bool Execute() + virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE { return that_.HandleTrailingStep(); } - virtual void Serialize(Json::Value& target) const + virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE { target = Json::nullValue; } diff -r c38b82bb6fd3 -r 320a2d224902 Core/SerializationToolbox.cpp --- a/Core/SerializationToolbox.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/SerializationToolbox.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -320,6 +320,28 @@ } + void WriteListOfStrings(Json::Value& target, + const std::list& values, + const std::string& field) + { + if (target.type() != Json::objectValue || + target.isMember(field.c_str())) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + Json::Value& value = target[field]; + + value = Json::arrayValue; + + for (std::list::const_iterator it = values.begin(); + it != values.end(); ++it) + { + value.append(*it); + } + } + + void WriteSetOfStrings(Json::Value& target, const std::set& values, const std::string& field) diff -r c38b82bb6fd3 -r 320a2d224902 Core/SerializationToolbox.h --- a/Core/SerializationToolbox.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/SerializationToolbox.h Wed Apr 01 10:15:33 2020 +0200 @@ -83,6 +83,10 @@ const std::vector& values, const std::string& field); + void WriteListOfStrings(Json::Value& target, + const std::list& values, + const std::string& field); + void WriteSetOfStrings(Json::Value& target, const std::set& values, const std::string& field); diff -r c38b82bb6fd3 -r 320a2d224902 Core/SharedLibrary.cpp --- a/Core/SharedLibrary.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/SharedLibrary.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -62,14 +62,19 @@ } #elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__) - + /** * "RTLD_LOCAL" is the default, and is only present to be * explicit. "RTLD_DEEPBIND" was added in Orthanc 1.6.0, in order * to avoid crashes while loading plugins from the LSB binaries of * the Orthanc core. + * + * BUT this had no effect, and this results in a crash if loading + * the Python 2.7 plugin => We disabled it again in Orthanc 1.6.1. **/ -#if defined(RTLD_DEEPBIND) // This is a GNU extension + +#if 0 // && defined(RTLD_DEEPBIND) // This is a GNU extension + // Disabled in Orthanc 1.6.1 handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND); #else handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL); diff -r c38b82bb6fd3 -r 320a2d224902 Core/Toolbox.cpp --- a/Core/Toolbox.cpp Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Toolbox.cpp Wed Apr 01 10:15:33 2020 +0200 @@ -2078,6 +2078,108 @@ throw OrthancException(ErrorCode_BadFileFormat, "Invalid UTF-8 string"); } } + + + std::string Toolbox::LargeHexadecimalToDecimal(const std::string& hex) + { + /** + * NB: Focus of the code below is *not* efficiency, but + * readability! + **/ + + for (size_t i = 0; i < hex.size(); i++) + { + const char c = hex[i]; + if (!((c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f') || + (c >= '0' && c <= '9'))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, + "Not an hexadecimal number"); + } + } + + std::vector decimal; + decimal.push_back(0); + + for (size_t i = 0; i < hex.size(); i++) + { + uint8_t hexDigit = static_cast(Hex2Dec(hex[i])); + assert(hexDigit <= 15); + + for (size_t j = 0; j < decimal.size(); j++) + { + uint8_t val = static_cast(decimal[j]) * 16 + hexDigit; // Maximum: 9 * 16 + 15 + assert(val <= 159 /* == 9 * 16 + 15 */); + + decimal[j] = val % 10; + hexDigit = val / 10; + assert(hexDigit <= 15 /* == 159 / 10 */); + } + + while (hexDigit > 0) + { + decimal.push_back(hexDigit % 10); + hexDigit /= 10; + } + } + + size_t start = 0; + while (start < decimal.size() && + decimal[start] == '0') + { + start++; + } + + std::string s; + s.reserve(decimal.size() - start); + + for (size_t i = decimal.size(); i > start; i--) + { + s.push_back(decimal[i - 1] + '0'); + } + + return s; + } + + + std::string Toolbox::GenerateDicomPrivateUniqueIdentifier() + { + /** + * REFERENCE: "Creating a Privately Defined Unique Identifier + * (Informative)" / "UUID Derived UID" + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html + * https://stackoverflow.com/a/46316162/881731 + **/ + + std::string uuid = GenerateUuid(); + assert(IsUuid(uuid) && uuid.size() == 36); + + /** + * After removing the four dashes ("-") out of the 36-character + * UUID, we get a large hexadecimal number with 32 characters, + * each of those characters lying in the range [0,16[. The large + * number is thus in the [0,16^32[ = [0,256^16[ range. This number + * has a maximum of 39 decimal digits, as can be seen in Python: + * + * # python -c 'import math; print(math.log(16**32))/math.log(10))' + * 38.531839445 + * + * We now to convert the large hexadecimal number to a decimal + * number with up to 39 digits, remove the leading zeros, then + * prefix it with "2.25." + **/ + + // Remove the dashes + std::string hex = (uuid.substr(0, 8) + + uuid.substr(9, 4) + + uuid.substr(14, 4) + + uuid.substr(19, 4) + + uuid.substr(24, 12)); + assert(hex.size() == 32); + + return "2.25." + LargeHexadecimalToDecimal(hex); + } } diff -r c38b82bb6fd3 -r 320a2d224902 Core/Toolbox.h --- a/Core/Toolbox.h Wed Apr 01 10:14:49 2020 +0200 +++ b/Core/Toolbox.h Wed Apr 01 10:15:33 2020 +0200 @@ -257,6 +257,11 @@ size_t& utf8Length, const std::string& utf8, size_t position); + + std::string LargeHexadecimalToDecimal(const std::string& hex); + + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html + std::string GenerateDicomPrivateUniqueIdentifier(); } } diff -r c38b82bb6fd3 -r 320a2d224902 NEWS --- a/NEWS Wed Apr 01 10:14:49 2020 +0200 +++ b/NEWS Wed Apr 01 10:15:33 2020 +0200 @@ -1,24 +1,47 @@ Pending changes in the mainline =============================== + +Maintenance +----------- + +* Source code repository moved from BitBucket to self-hosted server +* Fix lookup form in Orthanc Explorer (wildcards not allowed in StudyDate) +* Fix signature of "OrthancPluginRegisterStorageCommitmentScpCallback()" in plugins SDK + + +Version 1.6.0 (2020-03-18) +========================== + +General +------- + +* Support of DICOM storage commitment + REST API -------- * API version has been upgraded to 5 -* added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually - retrieve its version number) -* "/changes": Allow the "limit" argument to be greater than 100 -* "/instances/{id}/preview": Now takes the windowing into account -* "/tools/log-level": Possibility to access and change the log level without restarting Orthanc -* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes - to render frames, taking windowing and resizing into account -* "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files -* ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags +* Added: + - "/peers/{id}/system": Test the connectivity with a remote peer + (and also retrieve its version number) + - "/tools/log-level": Access and/or change the log level without restarting Orthanc + - "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered": + Render frames, taking windowing and resizing into account + - "/modalities/{...}/storage-commitment": Trigger storage commitment SCU + - "/storage-commitment/{...}": Access storage commitment reports + - "/storage-commitment/{...}/remove": Remove instances from storage commitment reports +* Improved: + - "/changes": Allow the "limit" argument to be greater than 100 + - "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files + - ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags + - "/modalities/{...}/store": New Boolean argument "StorageCommitment" Plugins ------- * New sample plugin: "ConnectivityChecks" +* New primitives to handle storage commitment SCP by plugins Lua --- @@ -32,6 +55,7 @@ Maintenance ----------- +* New configuration options: "DefaultPrivateCreator" and "StorageCommitmentReportsSize" * Support of MPEG4 transfer syntaxes in C-Store SCP * C-FIND SCU at Instance level now sets the 0008,0052 tag to IMAGE per default (was INSTANCE). Therefore, the "ClearCanvas" and "Dcm4Chee" modality manufacturer have now been deprecated. diff -r c38b82bb6fd3 -r 320a2d224902 OrthancExplorer/explorer.js --- a/OrthancExplorer/explorer.js Wed Apr 01 10:14:49 2020 +0200 +++ b/OrthancExplorer/explorer.js Wed Apr 01 10:15:33 2020 +0200 @@ -431,7 +431,7 @@ // NB: "GenerateDicomDate()" is defined in "query-retrieve.js" var target = $('#lookup-study-date'); $('option', target).remove(); - target.append($('