# HG changeset patch # User Sebastien Jodogne # Date 1589985764 -7200 # Node ID 6e14f2da7c7e338f67c2ec5a27cad7cf7738c2fc # Parent fe0e4ef52a7226e622fe6ec929fb228ee406ae9a# Parent 5fe8c6d3212e92653a5948cbb4e605c366e5659a integration transcoding->mainline diff -r fe0e4ef52a72 -r 6e14f2da7c7e .hgignore --- a/.hgignore Wed May 06 08:40:48 2020 +0200 +++ b/.hgignore Wed May 20 16:42:44 2020 +0200 @@ -6,3 +6,10 @@ .vs/ .vscode/ *~ + +# when opening Orthanc in VSCode, it might find a java project and create files we wan't to ignore: +.settings/ +.classpath +.project +Resources/Testing/Issue32/Java/bin +Resources/Testing/Issue32/Java/target diff -r fe0e4ef52a72 -r 6e14f2da7c7e CMakeLists.txt --- a/CMakeLists.txt Wed May 06 08:40:48 2020 +0200 +++ b/CMakeLists.txt Wed May 20 16:42:44 2020 +0200 @@ -13,6 +13,7 @@ set(ENABLE_CRYPTO_OPTIONS ON) set(ENABLE_DCMTK ON) set(ENABLE_DCMTK_NETWORKING ON) +set(ENABLE_DCMTK_TRANSCODING ON) set(ENABLE_GOOGLE_TEST ON) set(ENABLE_JPEG ON) set(ENABLE_LOCALE ON) @@ -25,9 +26,6 @@ set(ENABLE_WEB_SERVER ON) set(ENABLE_ZLIB ON) -# To test transcoding -#set(ENABLE_DCMTK_TRANSCODING ON) - set(HAS_EMBEDDED_RESOURCES ON) @@ -92,6 +90,7 @@ OrthancServer/ServerEnumerations.cpp OrthancServer/ServerIndex.cpp OrthancServer/ServerJobs/ArchiveJob.cpp + OrthancServer/ServerJobs/CleaningInstancesJob.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/ServerJobs/DicomMoveScuJob.cpp OrthancServer/ServerJobs/LuaJobManager.cpp diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/Compression/HierarchicalZipWriter.h --- a/Core/Compression/HierarchicalZipWriter.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/Compression/HierarchicalZipWriter.h Wed May 20 16:42:44 2020 +0200 @@ -140,7 +140,7 @@ return indexer_.GetCurrentDirectoryPath(); } - void Write(const char* data, size_t length) + void Write(const void* data, size_t length) { writer_.Write(data, length); } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/Compression/ZipWriter.cpp --- a/Core/Compression/ZipWriter.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/Compression/ZipWriter.cpp Wed May 20 16:42:44 2020 +0200 @@ -227,7 +227,7 @@ } - void ZipWriter::Write(const char* data, size_t length) + void ZipWriter::Write(const void* data, size_t length) { if (!hasFileInZip_) { @@ -236,17 +236,19 @@ const size_t maxBytesInAStep = std::numeric_limits::max(); + const char* p = reinterpret_cast(data); + while (length > 0) { int bytes = static_cast(length <= maxBytesInAStep ? length : maxBytesInAStep); - if (zipWriteInFileInZip(pimpl_->file_, data, bytes)) + if (zipWriteInFileInZip(pimpl_->file_, p, bytes)) { throw OrthancException(ErrorCode_CannotWriteFile, "Cannot write data to ZIP archive: " + path_); } - data += bytes; + p += bytes; length -= bytes; } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/Compression/ZipWriter.h --- a/Core/Compression/ZipWriter.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/Compression/ZipWriter.h Wed May 20 16:42:44 2020 +0200 @@ -101,7 +101,7 @@ void OpenFile(const char* path); - void Write(const char* data, size_t length); + void Write(const void* data, size_t length); void Write(const std::string& data); }; diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomAssociation.cpp --- a/Core/DicomNetworking/DicomAssociation.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/DicomAssociation.cpp Wed May 20 16:42:44 2020 +0200 @@ -259,10 +259,10 @@ LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << parameters.GetLocalApplicationEntityTitle() - << "\" to AET \"" << parameters.GetRemoteApplicationEntityTitle() - << "\" on host " << parameters.GetRemoteHost() - << ":" << parameters.GetRemotePort() - << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")"; + << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle() + << "\" on host " << parameters.GetRemoteModality().GetHost() + << ":" << parameters.GetRemoteModality().GetPortNumber() + << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ")"; CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); @@ -270,7 +270,7 @@ // Set this application's title and the called application's title in the params CheckConnecting(parameters, ASC_setAPTitles( params_, parameters.GetLocalApplicationEntityTitle().c_str(), - parameters.GetRemoteApplicationEntityTitle().c_str(), NULL)); + parameters.GetRemoteModality().GetApplicationEntityTitle().c_str(), NULL)); // Set the network addresses of the local and remote entities char localHost[HOST_NAME_MAX]; @@ -284,7 +284,8 @@ snprintf #endif (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", - parameters.GetRemoteHost().c_str(), parameters.GetRemotePort()); + parameters.GetRemoteModality().GetHost().c_str(), + parameters.GetRemoteModality().GetPortNumber()); CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort)); @@ -339,7 +340,7 @@ else { LOG(WARNING) << "Unknown transfer syntax received from AET \"" - << parameters.GetRemoteApplicationEntityTitle() + << parameters.GetRemoteModality().GetApplicationEntityTitle() << "\": " << pc->acceptedTransferSyntax; } } @@ -352,7 +353,7 @@ { throw OrthancException(ErrorCode_NoPresentationContext, "Unable to negotiate a presentation context with AET \"" + - parameters.GetRemoteApplicationEntityTitle() + "\""); + parameters.GetRemoteModality().GetApplicationEntityTitle() + "\""); } } @@ -517,7 +518,7 @@ throw OrthancException(ErrorCode_NetworkProtocol, "DicomAssociation - " + command + " to AET \"" + - parameters.GetRemoteApplicationEntityTitle() + + parameters.GetRemoteModality().GetApplicationEntityTitle() + "\": " + info); } } @@ -604,7 +605,7 @@ **/ LOG(INFO) << "Reporting modality \"" - << parameters.GetRemoteApplicationEntityTitle() + << parameters.GetRemoteModality().GetApplicationEntityTitle() << "\" about storage commitment transaction: " << transactionUid << " (" << successSopClassUids.size() << " successes, " << failedSopClassUids.size() << " failures)"; @@ -654,7 +655,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to send N-EVENT-REPORT request to AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (!DIMSE_sendMessageUsingMemoryData( @@ -682,7 +683,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to read N-EVENT-REPORT response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP; @@ -696,14 +697,14 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Badly formatted N-EVENT-REPORT response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (content.DimseStatus != 0 /* success */) { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "The request cannot be handled by remote AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } } @@ -767,7 +768,7 @@ **/ LOG(INFO) << "Request to modality \"" - << parameters.GetRemoteApplicationEntityTitle() + << parameters.GetRemoteModality().GetApplicationEntityTitle() << "\" about storage commitment for " << sopClassUids.size() << " instances, with transaction UID: " << transactionUid; const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++; @@ -801,7 +802,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to send N-ACTION request to AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (!DIMSE_sendMessageUsingMemoryData( @@ -829,7 +830,7 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Unable to read N-ACTION response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP; @@ -843,14 +844,14 @@ { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "Badly formatted N-ACTION response from AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } if (content.DimseStatus != 0 /* success */) { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "The request cannot be handled by remote AET: " + - parameters.GetRemoteApplicationEntityTitle()); + parameters.GetRemoteModality().GetApplicationEntityTitle()); } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomAssociationParameters.cpp --- a/Core/DicomNetworking/DicomAssociationParameters.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/DicomAssociationParameters.cpp Wed May 20 16:42:44 2020 +0200 @@ -37,6 +37,7 @@ #include "../Compatibility.h" #include "../Logging.h" #include "../OrthancException.h" +#include "../SerializationToolbox.h" #include "NetworkingCompatibility.h" #include @@ -48,68 +49,104 @@ namespace Orthanc { - void DicomAssociationParameters::ReadDefaultTimeout() + void DicomAssociationParameters::CheckHost(const std::string& host) + { + if (host.size() > HOST_NAME_MAX - 10) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Invalid host name (too long): " + host); + } + } + + + uint32_t DicomAssociationParameters::GetDefaultTimeout() { boost::mutex::scoped_lock lock(defaultTimeoutMutex_); - timeout_ = defaultTimeout_; + return defaultTimeout_; } DicomAssociationParameters::DicomAssociationParameters() : - localAet_("STORESCU"), - remoteAet_("ANY-SCP"), - remoteHost_("127.0.0.1"), - remotePort_(104), - manufacturer_(ModalityManufacturer_Generic) + localAet_("ORTHANC"), + timeout_(GetDefaultTimeout()) { - ReadDefaultTimeout(); + remote_.SetApplicationEntityTitle("ANY-SCP"); } DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet, const RemoteModalityParameters& remote) : localAet_(localAet), - remoteAet_(remote.GetApplicationEntityTitle()), - remoteHost_(remote.GetHost()), - remotePort_(remote.GetPortNumber()), - manufacturer_(remote.GetManufacturer()), - timeout_(defaultTimeout_) + timeout_(GetDefaultTimeout()) { - ReadDefaultTimeout(); + SetRemoteModality(remote); } - void DicomAssociationParameters::SetRemoteHost(const std::string& host) + void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& remote) { - if (host.size() > HOST_NAME_MAX - 10) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Invalid host name (too long): " + host); - } - - remoteHost_ = host; + CheckHost(remote.GetHost()); + remote_ = remote; } - void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& parameters) + void DicomAssociationParameters::SetRemoteHost(const std::string& host) { - SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); - SetRemoteHost(parameters.GetHost()); - SetRemotePort(parameters.GetPortNumber()); - SetRemoteManufacturer(parameters.GetManufacturer()); + CheckHost(host); + remote_.SetHost(host); } bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const { return (localAet_ == other.localAet_ && - remoteAet_ == other.remoteAet_ && - remoteHost_ == other.remoteHost_ && - remotePort_ == other.remotePort_ && - manufacturer_ == other.manufacturer_); + remote_.GetApplicationEntityTitle() == other.remote_.GetApplicationEntityTitle() && + remote_.GetHost() == other.remote_.GetHost() && + remote_.GetPortNumber() == other.remote_.GetPortNumber() && + remote_.GetManufacturer() == other.remote_.GetManufacturer() && + timeout_ == other.timeout_); } + + static const char* const LOCAL_AET = "LocalAet"; + static const char* const REMOTE = "Remote"; + static const char* const TIMEOUT = "Timeout"; // New in Orthanc in 1.7.0 + + + void DicomAssociationParameters::SerializeJob(Json::Value& target) const + { + if (target.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + target[LOCAL_AET] = localAet_; + remote_.Serialize(target[REMOTE], true /* force advanced format */); + target[TIMEOUT] = timeout_; + } + } + + + DicomAssociationParameters DicomAssociationParameters::UnserializeJob(const Json::Value& serialized) + { + if (serialized.type() == Json::objectValue) + { + DicomAssociationParameters result; + result.remote_ = RemoteModalityParameters(serialized[REMOTE]); + result.localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); + result.timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, GetDefaultTimeout()); + + return result; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + + void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds) { LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomAssociationParameters.h --- a/Core/DicomNetworking/DicomAssociationParameters.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/DicomAssociationParameters.h Wed May 20 16:42:44 2020 +0200 @@ -35,6 +35,8 @@ #include "RemoteModalityParameters.h" +#include + class OFCondition; // From DCMTK namespace Orthanc @@ -42,14 +44,11 @@ class DicomAssociationParameters { private: - std::string localAet_; - std::string remoteAet_; - std::string remoteHost_; - uint16_t remotePort_; - ModalityManufacturer manufacturer_; - uint32_t timeout_; + std::string localAet_; + RemoteModalityParameters remote_; + uint32_t timeout_; - void ReadDefaultTimeout(); + static void CheckHost(const std::string& host); public: DicomAssociationParameters(); @@ -62,52 +61,38 @@ return localAet_; } - const std::string& GetRemoteApplicationEntityTitle() const - { - return remoteAet_; - } - - const std::string& GetRemoteHost() const - { - return remoteHost_; - } - - uint16_t GetRemotePort() const - { - return remotePort_; - } - - ModalityManufacturer GetRemoteManufacturer() const - { - return manufacturer_; - } - void SetLocalApplicationEntityTitle(const std::string& aet) { localAet_ = aet; } + const RemoteModalityParameters& GetRemoteModality() const + { + return remote_; + } + + void SetRemoteModality(const RemoteModalityParameters& parameters); + void SetRemoteApplicationEntityTitle(const std::string& aet) { - remoteAet_ = aet; + remote_.SetApplicationEntityTitle(aet); } void SetRemoteHost(const std::string& host); void SetRemotePort(uint16_t port) { - remotePort_ = port; + remote_.SetPortNumber(port); } void SetRemoteManufacturer(ModalityManufacturer manufacturer) { - manufacturer_ = manufacturer; + remote_.SetManufacturer(manufacturer); } - void SetRemoteModality(const RemoteModalityParameters& parameters); - bool IsEqual(const DicomAssociationParameters& other) const; + // Setting it to "0" disables the timeout (infinite wait) void SetTimeout(uint32_t seconds) { timeout_ = seconds; @@ -122,7 +107,13 @@ { return timeout_ != 0; } + + void SerializeJob(Json::Value& target) const; + + static DicomAssociationParameters UnserializeJob(const Json::Value& serialized); static void SetDefaultTimeout(uint32_t seconds); + + static uint32_t GetDefaultTimeout(); }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomControlUserConnection.cpp --- a/Core/DicomNetworking/DicomControlUserConnection.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/DicomControlUserConnection.cpp Wed May 20 16:42:44 2020 +0200 @@ -257,7 +257,7 @@ if (presID == 0) { throw OrthancException(ErrorCode_DicomFindUnavailable, - "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); + "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); } T_DIMSE_C_FindRQ request; @@ -309,14 +309,14 @@ throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-FIND SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf + " (unable to process - invalid query ?)"); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } @@ -331,7 +331,7 @@ association_->Open(parameters_); std::unique_ptr query( - ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); + ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer())); DcmDataset* dataset = query->GetDcmtkObject().getDataset(); const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; @@ -362,7 +362,7 @@ if (presID == 0) { throw OrthancException(ErrorCode_DicomMoveUnavailable, - "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); + "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); } T_DIMSE_C_MoveRQ request; @@ -412,14 +412,14 @@ throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-MOVE SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf + " (unable to process - resource not found ?)"); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" + - parameters_.GetRemoteApplicationEntityTitle() + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } @@ -434,15 +434,6 @@ } - DicomControlUserConnection::DicomControlUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote) : - parameters_(localAet, remote), - association_(new DicomAssociation) - { - SetupPresentationContexts(); - } - - void DicomControlUserConnection::Close() { assert(association_.get() != NULL); @@ -479,7 +470,7 @@ { DicomMap fields; NormalizeFindQuery(fields, level, originalFields); - query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); + query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer())); } else { @@ -525,7 +516,7 @@ const char* universal; - if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE) + if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE) { universal = "*"; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomControlUserConnection.h --- a/Core/DicomNetworking/DicomControlUserConnection.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/DicomControlUserConnection.h Wed May 20 16:42:44 2020 +0200 @@ -65,9 +65,6 @@ const DicomMap& fields); public: - DicomControlUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote); - DicomControlUserConnection(const DicomAssociationParameters& params); const DicomAssociationParameters& GetParameters() const diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomStoreUserConnection.cpp --- a/Core/DicomNetworking/DicomStoreUserConnection.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp Wed May 20 16:42:44 2020 +0200 @@ -48,8 +48,28 @@ bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid, const std::set& syntaxes) { + // Default transfer syntax for DICOM + const bool addLittleEndianImplicit = ( + proposeUncompressedSyntaxes_ && + syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end()); + + const bool addLittleEndianExplicit = ( + proposeUncompressedSyntaxes_ && + syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end()); + + const bool addBigEndianExplicit = ( + proposeUncompressedSyntaxes_ && + proposeRetiredBigEndian_ && + syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end()); + size_t requiredCount = syntaxes.size(); - if (proposeUncompressedSyntaxes_) + if (addLittleEndianImplicit) + { + requiredCount += 1; + } + + if (addLittleEndianExplicit || + addBigEndianExplicit) { requiredCount += 1; } @@ -58,40 +78,49 @@ { return false; // Not enough room } - - for (std::set::const_iterator - it = syntaxes.begin(); it != syntaxes.end(); ++it) + else { - association_->ProposePresentationContext(sopClassUid, *it); - } - - if (proposeUncompressedSyntaxes_) - { - std::set uncompressed; - - if (syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end()) + for (std::set::const_iterator + it = syntaxes.begin(); it != syntaxes.end(); ++it) { - uncompressed.insert(DicomTransferSyntax_LittleEndianImplicit); + association_->ProposePresentationContext(sopClassUid, *it); + proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *it)); } - - if (syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end()) + + if (addLittleEndianImplicit) { - uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit); - } - - if (proposeRetiredBigEndian_ && - syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end()) - { - uncompressed.insert(DicomTransferSyntax_BigEndianExplicit); + association_->ProposePresentationContext(sopClassUid, DicomTransferSyntax_LittleEndianImplicit); + proposedOriginalClasses_.insert(std::make_pair(sopClassUid, DicomTransferSyntax_LittleEndianImplicit)); } - if (!uncompressed.empty()) + if (addLittleEndianExplicit || + addBigEndianExplicit) { + std::set uncompressed; + + if (addLittleEndianExplicit) + { + uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit); + } + + if (addBigEndianExplicit) + { + uncompressed.insert(DicomTransferSyntax_BigEndianExplicit); + } + association_->ProposePresentationContext(sopClassUid, uncompressed); + + assert(!uncompressed.empty()); + if (addLittleEndianExplicit ^ addBigEndianExplicit) + { + // Only one transfer syntax was proposed for this presentation context + assert(uncompressed.size() == 1); + proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *uncompressed.begin())); + } } - } - return true; + return true; + } } @@ -116,8 +145,8 @@ return false; } - - + + DicomStoreUserConnection::DicomStoreUserConnection( const DicomAssociationParameters& params) : parameters_(params), @@ -129,16 +158,16 @@ } - void DicomStoreUserConnection::PrepareStorageClass(const std::string& sopClassUid, - DicomTransferSyntax syntax) + void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid, + DicomTransferSyntax syntax) { - StorageClasses::iterator found = storageClasses_.find(sopClassUid); + RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid); - if (found == storageClasses_.end()) + if (found == registeredClasses_.end()) { std::set ts; ts.insert(syntax); - storageClasses_[sopClassUid] = ts; + registeredClasses_[sopClassUid] = ts; } else { @@ -147,6 +176,36 @@ } + void DicomStoreUserConnection::LookupParameters(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomTransferSyntax& transferSyntax, + DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + OFString a, b; + if (!dicom.getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() || + !dicom.getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good()) + { + throw OrthancException(ErrorCode_NoSopClassOrInstance, + "Unable to determine the SOP class/instance for C-STORE with AET " + + parameters_.GetRemoteModality().GetApplicationEntityTitle()); + } + + sopClassUid.assign(a.c_str()); + sopInstanceUid.assign(b.c_str()); + + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom)) + { + throw OrthancException(ErrorCode_InternalError, + "Unknown transfer syntax from DCMTK"); + } + } + + bool DicomStoreUserConnection::NegotiatePresentationContext( uint8_t& presentationContextId, const std::string& sopClassUid, @@ -154,7 +213,7 @@ { /** * Step 1: Check whether this presentation context is already - * available in the previously negociated assocation. + * available in the previously negotiated assocation. **/ if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax)) @@ -163,22 +222,37 @@ } // The association must be re-negotiated - LOG(INFO) << "Re-negociating DICOM association with " - << parameters_.GetRemoteApplicationEntityTitle(); + if (association_->IsOpen()) + { + LOG(INFO) << "Re-negotiating DICOM association with " + << parameters_.GetRemoteModality().GetApplicationEntityTitle(); + + if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) != + proposedOriginalClasses_.end()) + { + LOG(INFO) << "The remote modality has already rejected SOP class UID \"" + << sopClassUid << "\" with transfer syntax \"" + << GetTransferSyntaxUid(transferSyntax) << "\", don't renegotiate"; + return false; + } + } + association_->ClearPresentationContexts(); - PrepareStorageClass(sopClassUid, transferSyntax); + proposedOriginalClasses_.clear(); + RegisterStorageClass(sopClassUid, transferSyntax); // (*) - + /** * Step 2: Propose at least the mandatory SOP class. **/ { - StorageClasses::const_iterator mandatory = storageClasses_.find(sopClassUid); + RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid); - if (mandatory == storageClasses_.end() || + if (mandatory == registeredClasses_.end() || mandatory->second.find(transferSyntax) == mandatory->second.end()) { + // Should never fail because of (*) throw OrthancException(ErrorCode_InternalError); } @@ -194,11 +268,11 @@ /** * Step 3: Propose all the previously spotted SOP classes, as - * registered through the "PrepareStorageClass()" method. + * registered through the "RegisterStorageClass()" method. **/ - for (StorageClasses::const_iterator it = storageClasses_.begin(); - it != storageClasses_.end(); ++it) + for (RegisteredClasses::const_iterator it = registeredClasses_.begin(); + it != registeredClasses_.end(); ++it) { if (it->first != sopClassUid) { @@ -217,15 +291,16 @@ if (proposeCommonClasses_) { + // The method "ProposeStorageClass()" will automatically add + // "LittleEndianImplicit" std::set ts; - ts.insert(DicomTransferSyntax_LittleEndianImplicit); for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++) { std::string c(dcmShortSCUStorageSOPClassUIDs[i]); if (c != sopClassUid && - storageClasses_.find(c) == storageClasses_.end()) + registeredClasses_.find(c) == registeredClasses_.end()) { ProposeStorageClass(c, ts); } @@ -245,36 +320,23 @@ void DicomStoreUserConnection::Store(std::string& sopClassUid, std::string& sopInstanceUid, - DcmDataset& dataset, + DcmFileFormat& dicom, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { - OFString a, b; - if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() || - !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good()) - { - throw OrthancException(ErrorCode_NoSopClassOrInstance, - "Unable to determine the SOP class/instance for C-STORE with AET " + - parameters_.GetRemoteApplicationEntityTitle()); - } - - sopClassUid.assign(a.c_str()); - sopInstanceUid.assign(b.c_str()); + DicomTransferSyntax transferSyntax; + LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom); - DicomTransferSyntax transferSyntax; - if (!FromDcmtkBridge::LookupOrthancTransferSyntax( - transferSyntax, dataset.getOriginalXfer())) + uint8_t presID; + if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax)) { - throw OrthancException(ErrorCode_InternalError, - "Unknown transfer syntax from DCMTK"); - } - - // Figure out which accepted presentation context should be used - uint8_t presID; - if (!NegotiatePresentationContext(presID, sopClassUid.c_str(), transferSyntax)) - { - throw OrthancException(ErrorCode_InternalError, - "No valid presentation context was negotiated upfront"); + throw OrthancException(ErrorCode_NetworkProtocol, + "No valid presentation context was negotiated for " + "SOP class UID [" + sopClassUid + "] and transfer " + "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] " + "while sending to modality [" + + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]"); } // Prepare the transmission of data @@ -286,8 +348,8 @@ request.DataSetType = DIMSE_DATASET_PRESENT; strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN); - if (!moveOriginatorAET.empty()) - { + if (hasMoveOriginator) + { strncpy(request.MoveOriginatorApplicationEntityTitle, moveOriginatorAET.c_str(), DIC_AE_LEN); request.opts = O_STORE_MOVEORIGINATORAETITLE; @@ -296,12 +358,17 @@ request.opts |= O_STORE_MOVEORIGINATORID; } + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + // Finally conduct transmission of data T_DIMSE_C_StoreRSP response; DcmDataset* statusDetail = NULL; DicomAssociation::CheckCondition( DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request, - NULL, &dataset, /*progressCallback*/ NULL, NULL, + NULL, dicom.getDataset(), /*progressCallback*/ NULL, NULL, /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), /*opt_dimse_timeout*/ GetParameters().GetTimeout(), &response, &statusDetail, NULL), @@ -326,7 +393,7 @@ sprintf(buf, "%04X", response.DimseStatus); throw OrthancException(ErrorCode_NetworkProtocol, "C-STORE SCU to AET \"" + - GetParameters().GetRemoteApplicationEntityTitle() + + GetParameters().GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } @@ -334,26 +401,127 @@ void DicomStoreUserConnection::Store(std::string& sopClassUid, std::string& sopInstanceUid, - ParsedDicomFile& parsed, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - Store(sopClassUid, sopInstanceUid, *parsed.GetDcmtkObject().getDataset(), - moveOriginatorAET, moveOriginatorID); - } - - - void DicomStoreUserConnection::Store(std::string& sopClassUid, - std::string& sopInstanceUid, const void* buffer, size_t size, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID) { std::unique_ptr dicom( FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size)); - Store(sopClassUid, sopInstanceUid, *dicom->getDataset(), - moveOriginatorAET, moveOriginatorID); + if (dicom.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + } + + + void DicomStoreUserConnection::LookupTranscoding(std::set& acceptedSyntaxes, + const std::string& sopClassUid, + DicomTransferSyntax sourceSyntax) + { + acceptedSyntaxes.clear(); + + // Make sure a negotiation has already occurred for this transfer + // syntax. We don't use the return code: Transcoding is possible + // even if the "sourceSyntax" is not supported. + uint8_t presID; + NegotiatePresentationContext(presID, sopClassUid, sourceSyntax); + + std::map contexts; + if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid)) + { + for (std::map::const_iterator + it = contexts.begin(); it != contexts.end(); ++it) + { + acceptedSyntaxes.insert(it->first); + } + } + } + + + void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + IDicomTranscoder& transcoder, + const void* buffer, + size_t size, + bool hasMoveOriginator, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID) + { + std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size)); + if (dicom.get() == NULL || + dicom->getDataset() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DicomTransferSyntax inputSyntax; + LookupParameters(sopClassUid, sopInstanceUid, inputSyntax, *dicom); + + std::set accepted; + LookupTranscoding(accepted, sopClassUid, inputSyntax); + + if (accepted.find(inputSyntax) != accepted.end()) + { + // No need for transcoding + Store(sopClassUid, sopInstanceUid, *dicom, + hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + } + else + { + // Transcoding is needed + std::set uncompressedSyntaxes; + + if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end()) + { + uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + } + + if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end()) + { + uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); + } + + if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end()) + { + uncompressedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit); + } + + IDicomTranscoder::DicomImage source; + source.AcquireParsed(dicom.release()); + source.SetExternalBuffer(buffer, size); + + const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed()); + + IDicomTranscoder::DicomImage transcoded; + if (transcoder.Transcode(transcoded, source, uncompressedSyntaxes, false)) + { + if (sourceUid != IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed())) + { + throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP " + "instance UID while transcoding to an uncompressed transfer syntax"); + } + else + { + DicomTransferSyntax transcodedSyntax; + + // Sanity check + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) || + accepted.find(transcodedSyntax) == accepted.end()) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(), + hasMoveOriginator, moveOriginatorAET, moveOriginatorID); + } + } + } + } } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomStoreUserConnection.h --- a/Core/DicomNetworking/DicomStoreUserConnection.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/DicomStoreUserConnection.h Wed May 20 16:42:44 2020 +0200 @@ -33,15 +33,23 @@ #pragma once +#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING) +# error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file +#endif + #include "DicomAssociationParameters.h" +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 +# include "../DicomParsing/IDicomTranscoder.h" +#endif + #include #include #include #include // For uint8_t -class DcmDataset; +class DcmFileFormat; namespace Orthanc { @@ -64,16 +72,20 @@ **/ class DicomAssociation; // Forward declaration for PImpl design pattern - class ParsedDicomFile; class DicomStoreUserConnection : public boost::noncopyable { private: - typedef std::map > StorageClasses; + typedef std::map > RegisteredClasses; + + // "ProposedOriginalClasses" keeps track of the storage classes + // that were proposed with a single transfer syntax + typedef std::set< std::pair > ProposedOriginalClasses; DicomAssociationParameters parameters_; - boost::shared_ptr association_; - StorageClasses storageClasses_; + boost::shared_ptr association_; // "shared_ptr" is for PImpl + RegisteredClasses registeredClasses_; + ProposedOriginalClasses proposedOriginalClasses_; bool proposeCommonClasses_; bool proposeUncompressedSyntaxes_; bool proposeRetiredBigEndian_; @@ -81,11 +93,18 @@ // Return "false" if there is not enough room remaining in the association bool ProposeStorageClass(const std::string& sopClassUid, const std::set& syntaxes); - - // Should only be used if transcoding + bool LookupPresentationContext(uint8_t& presentationContextId, const std::string& sopClassUid, DicomTransferSyntax transferSyntax); + + bool NegotiatePresentationContext(uint8_t& presentationContextId, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax); + + void LookupTranscoding(std::set& acceptedSyntaxes, + const std::string& sopClassUid, + DicomTransferSyntax sourceSyntax); public: DicomStoreUserConnection(const DicomAssociationParameters& params); @@ -125,23 +144,13 @@ return proposeRetiredBigEndian_; } - void PrepareStorageClass(const std::string& sopClassUid, - DicomTransferSyntax syntax); - - // TODO => to private - bool NegotiatePresentationContext(uint8_t& presentationContextId, - const std::string& sopClassUid, - DicomTransferSyntax transferSyntax); + void RegisterStorageClass(const std::string& sopClassUid, + DicomTransferSyntax syntax); void Store(std::string& sopClassUid, std::string& sopInstanceUid, - DcmDataset& dataset, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void Store(std::string& sopClassUid, - std::string& sopInstanceUid, - ParsedDicomFile& parsed, + DcmFileFormat& dicom, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); @@ -149,7 +158,22 @@ std::string& sopInstanceUid, const void* buffer, size_t size, + bool hasMoveOriginator, const std::string& moveOriginatorAET, uint16_t moveOriginatorID); + + void LookupParameters(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomTransferSyntax& transferSyntax, + DcmFileFormat& dicom); + + void Transcode(std::string& sopClassUid /* out */, + std::string& sopInstanceUid /* out */, + IDicomTranscoder& transcoder, + const void* buffer, + size_t size, + bool hasMoveOriginator, + const std::string& moveOriginatorAET, + uint16_t moveOriginatorID); }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomUserConnection.cpp --- a/Core/DicomNetworking/DicomUserConnection.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1822 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - - -/*========================================================================= - - This file is based on portions of the following project: - - Program: DCMTK 3.6.0 - Module: http://dicom.offis.de/dcmtk.php.en - -Copyright (C) 1994-2011, OFFIS e.V. -All rights reserved. - -This software and supporting documentation were developed by - - OFFIS e.V. - R&D Division Health - Escherweg 2 - 26121 Oldenburg, Germany - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -- Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -- Neither the name of OFFIS nor the names of its contributors may be - used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -=========================================================================*/ - - -#include "../PrecompiledHeaders.h" -#include "DicomUserConnection.h" - -#if !defined(DCMTK_VERSION_NUMBER) -# error The macro DCMTK_VERSION_NUMBER must be defined -#endif - -#include "../Compatibility.h" -#include "../DicomFormat/DicomArray.h" -#include "../Logging.h" -#include "../OrthancException.h" -#include "../DicomParsing/FromDcmtkBridge.h" -#include "../DicomParsing/ToDcmtkBridge.h" -#include "NetworkingCompatibility.h" - -#include -#include -#include -#include -#include -#include - -#include - - -static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax; - -/** - * "If we have more than 64 storage SOP classes, tools such as - * storescu will fail because they attempt to negotiate two - * presentation contexts for each SOP class, and there is a total - * limit of 128 contexts for one association." - **/ -static const unsigned int MAXIMUM_STORAGE_SOP_CLASSES = 64; - - -namespace Orthanc -{ - // By default, the timeout for DICOM SCU (client) connections is set to 10 seconds - static uint32_t defaultTimeout_ = 10; - - struct DicomUserConnection::PImpl - { - // Connection state - uint32_t dimseTimeout_; - uint32_t acseTimeout_; - T_ASC_Network* net_; - T_ASC_Parameters* params_; - T_ASC_Association* assoc_; - - bool IsOpen() const - { - return assoc_ != NULL; - } - - void CheckIsOpen() const; - - void Store(std::string& sopClassUidOut /* out */, - std::string& sopInstanceUidOut /* out */, - DcmInputStream& is, - DicomUserConnection& connection, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - }; - - - static void Check(const OFCondition& cond, - const std::string& aet, - const std::string& command) - { - if (cond.bad()) - { - // Reformat the error message from DCMTK by turning multiline - // errors into a single line - - std::string s(cond.text()); - std::string info; - info.reserve(s.size()); - - bool isMultiline = false; - for (size_t i = 0; i < s.size(); i++) - { - if (s[i] == '\r') - { - // Ignore - } - else if (s[i] == '\n') - { - if (isMultiline) - { - info += "; "; - } - else - { - info += " ("; - isMultiline = true; - } - } - else - { - info.push_back(s[i]); - } - } - - if (isMultiline) - { - info += ")"; - } - - throw OrthancException(ErrorCode_NetworkProtocol, - "DicomUserConnection - " + command + - " to AET \"" + aet + "\": " + info); - } - } - - void DicomUserConnection::PImpl::CheckIsOpen() const - { - if (!IsOpen()) - { - throw OrthancException(ErrorCode_NetworkProtocol, - "DicomUserConnection: First open the connection"); - } - } - - - void DicomUserConnection::CheckIsOpen() const - { - pimpl_->CheckIsOpen(); - } - - - static void RegisterStorageSOPClass(T_ASC_Parameters* params, - unsigned int& presentationContextId, - const std::string& sopClass, - const char* asPreferred[], - std::vector& asFallback, - const std::string& aet) - { - Check(ASC_addPresentationContext(params, presentationContextId, - sopClass.c_str(), asPreferred, 1), - aet, "initializing"); - presentationContextId += 2; - - if (asFallback.size() > 0) - { - Check(ASC_addPresentationContext(params, presentationContextId, - sopClass.c_str(), &asFallback[0], asFallback.size()), - aet, "initializing"); - presentationContextId += 2; - } - } - - - void DicomUserConnection::SetupPresentationContexts(Mode mode, - const std::string& preferredTransferSyntax) - { - // Flatten an array with the preferred transfer syntax - const char* asPreferred[1] = { preferredTransferSyntax.c_str() }; - - // Setup the fallback transfer syntaxes - std::set fallbackSyntaxes; - fallbackSyntaxes.insert(UID_LittleEndianExplicitTransferSyntax); - fallbackSyntaxes.insert(UID_BigEndianExplicitTransferSyntax); - fallbackSyntaxes.insert(UID_LittleEndianImplicitTransferSyntax); - fallbackSyntaxes.erase(preferredTransferSyntax); - - // Flatten an array with the fallback transfer syntaxes - std::vector asFallback; - asFallback.reserve(fallbackSyntaxes.size()); - for (std::set::const_iterator - it = fallbackSyntaxes.begin(); it != fallbackSyntaxes.end(); ++it) - { - asFallback.push_back(it->c_str()); - } - - CheckStorageSOPClassesInvariant(); - - switch (mode) - { - case Mode_Generic: - { - unsigned int presentationContextId = 1; - - for (std::list::const_iterator it = reservedStorageSOPClasses_.begin(); - it != reservedStorageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } - - for (std::set::const_iterator it = storageSOPClasses_.begin(); - it != storageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } - - for (std::set::const_iterator it = defaultStorageSOPClasses_.begin(); - it != defaultStorageSOPClasses_.end(); ++it) - { - RegisterStorageSOPClass(pimpl_->params_, presentationContextId, - *it, asPreferred, asFallback, remoteAet_); - } - - break; - } - - case Mode_RequestStorageCommitment: - case Mode_ReportStorageCommitment: - { - const char* as = UID_StorageCommitmentPushModelSOPClass; - - std::vector ts; - ts.push_back(UID_LittleEndianExplicitTransferSyntax); - ts.push_back(UID_LittleEndianImplicitTransferSyntax); - - T_ASC_SC_ROLE role; - switch (mode) - { - case Mode_RequestStorageCommitment: - role = ASC_SC_ROLE_DEFAULT; - break; - - case Mode_ReportStorageCommitment: - role = ASC_SC_ROLE_SCP; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - Check(ASC_addPresentationContext(pimpl_->params_, 1 /*presentationContextId*/, - as, &ts[0], ts.size(), role), - remoteAet_, "initializing"); - - break; - } - - default: - throw OrthancException(ErrorCode_InternalError); - } - } - - - static bool IsGenericTransferSyntax(const std::string& syntax) - { - return (syntax == UID_LittleEndianExplicitTransferSyntax || - syntax == UID_BigEndianExplicitTransferSyntax || - syntax == UID_LittleEndianImplicitTransferSyntax); - } - - - void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut, - std::string& sopInstanceUidOut, - DcmInputStream& is, - DicomUserConnection& connection, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - DcmFileFormat dcmff; - Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength), - connection.remoteAet_, "C-STORE"); - - // Determine the storage SOP class UID for this instance - OFString sopClassUid; - if (dcmff.getDataset()->findAndGetOFString(DCM_SOPClassUID, sopClassUid).good()) - { - connection.AddStorageSOPClass(sopClassUid.c_str()); - } - - // Determine whether a new presentation context must be - // negotiated, depending on the transfer syntax of this instance - DcmXfer xfer(dcmff.getDataset()->getOriginalXfer()); - const std::string syntax(xfer.getXferID()); - bool isGeneric = IsGenericTransferSyntax(syntax); - - bool renegotiate; - - if (!IsOpen()) - { - renegotiate = true; - } - else if (isGeneric) - { - // Are we making a generic-to-specific or specific-to-generic change of - // the transfer syntax? If this is the case, renegotiate the connection. - renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax()); - - if (renegotiate) - { - LOG(INFO) << "Use of non-generic transfer syntax: the C-Store associated must be renegotiated"; - } - } - else - { - // We are using a specific transfer syntax. Renegotiate if the - // current connection does not match this transfer syntax. - renegotiate = (syntax != connection.GetPreferredTransferSyntax()); - - if (renegotiate) - { - LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated"; - } - } - - if (renegotiate) - { - if (isGeneric) - { - connection.ResetPreferredTransferSyntax(); - } - else - { - connection.SetPreferredTransferSyntax(syntax); - } - } - - if (!connection.IsOpen()) - { - connection.Open(); - } - - // Figure out which SOP class and SOP instance is encapsulated in the file - DIC_UI sopClass; - DIC_UI sopInstance; - -#if DCMTK_VERSION_NUMBER >= 364 - if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sizeof(sopClass), sopInstance, sizeof(sopInstance))) -#else - if (!DU_findSOPClassAndInstanceInDataSet(dcmff.getDataset(), sopClass, sopInstance)) -#endif - { - throw OrthancException(ErrorCode_NoSopClassOrInstance, - "Unable to determine the SOP class/instance for C-STORE with AET " + - connection.remoteAet_); - } - - sopClassUidOut.assign(sopClass); - sopInstanceUidOut.assign(sopInstance); - - // Figure out which of the accepted presentation contexts should be used - int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass); - if (presID == 0) - { - const char *modalityName = dcmSOPClassUIDToModality(sopClass); - if (modalityName == NULL) modalityName = dcmFindNameOfUID(sopClass); - if (modalityName == NULL) modalityName = "unknown SOP class"; - throw OrthancException(ErrorCode_NoPresentationContext, - "Unable to determine the accepted presentation contexts for C-STORE with AET " + - connection.remoteAet_ + " (" + std::string(modalityName) + ")"); - } - - // Prepare the transmission of data - T_DIMSE_C_StoreRQ request; - memset(&request, 0, sizeof(request)); - request.MessageID = assoc_->nextMsgID++; - strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); - request.Priority = DIMSE_PRIORITY_MEDIUM; - request.DataSetType = DIMSE_DATASET_PRESENT; - strncpy(request.AffectedSOPInstanceUID, sopInstance, DIC_UI_LEN); - - if (!moveOriginatorAET.empty()) - { - strncpy(request.MoveOriginatorApplicationEntityTitle, - moveOriginatorAET.c_str(), DIC_AE_LEN); - request.opts = O_STORE_MOVEORIGINATORAETITLE; - - request.MoveOriginatorID = moveOriginatorID; // The type DIC_US is an alias for uint16_t - request.opts |= O_STORE_MOVEORIGINATORID; - } - - // Finally conduct transmission of data - T_DIMSE_C_StoreRSP response; - DcmDataset* statusDetail = NULL; - Check(DIMSE_storeUser(assoc_, presID, &request, - NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL, - /*opt_blockMode*/ (dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ dimseTimeout_, - &response, &statusDetail, NULL), - connection.remoteAet_, "C-STORE"); - - if (statusDetail != NULL) - { - delete statusDetail; - } - - - /** - * New in Orthanc 1.6.0: Deal with failures during C-STORE. - * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1 - **/ - - if (response.DimseStatus != 0x0000 && // Success - response.DimseStatus != 0xB000 && // Warning - Coercion of Data Elements - response.DimseStatus != 0xB007 && // Warning - Data Set does not match SOP Class - response.DimseStatus != 0xB006) // Warning - Elements Discarded - { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - throw OrthancException(ErrorCode_NetworkProtocol, - "C-STORE SCU to AET \"" + connection.remoteAet_ + - "\" has failed with DIMSE status 0x" + buf); - } - } - - - namespace - { - struct FindPayload - { - DicomFindAnswers* answers; - const char* level; - bool isWorklist; - }; - } - - - static void FindCallback( - /* in */ - void *callbackData, - T_DIMSE_C_FindRQ *request, /* original find request */ - int responseCount, - T_DIMSE_C_FindRSP *response, /* pending response received */ - DcmDataset *responseIdentifiers /* pending response identifiers */ - ) - { - FindPayload& payload = *reinterpret_cast(callbackData); - - if (responseIdentifiers != NULL) - { - if (payload.isWorklist) - { - ParsedDicomFile answer(*responseIdentifiers); - payload.answers->Add(answer); - } - else - { - DicomMap m; - FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers); - - if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) - { - m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false); - } - - payload.answers->Add(m); - } - } - } - - - static void NormalizeFindQuery(DicomMap& fixedQuery, - ResourceType level, - const DicomMap& fields) - { - std::set allowedTags; - - // WARNING: Do not add "break" or reorder items in this switch-case! - switch (level) - { - case ResourceType_Instance: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance); - - case ResourceType_Series: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Series); - - case ResourceType_Study: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Study); - - case ResourceType_Patient: - DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (level) - { - case ResourceType_Patient: - allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); - allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); - allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); - break; - - case ResourceType_Study: - allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); - allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); - allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); - allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY); - break; - - case ResourceType_Series: - allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); - break; - - default: - break; - } - - allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); - - DicomArray query(fields); - for (size_t i = 0; i < query.GetSize(); i++) - { - const DicomTag& tag = query.GetElement(i).GetTag(); - if (allowedTags.find(tag) == allowedTags.end()) - { - LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag; - } - else - { - fixedQuery.SetValue(tag, query.GetElement(i).GetValue()); - } - } - } - - - static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields, - ModalityManufacturer manufacturer) - { - // Fix outgoing C-Find requests issue for Syngo.Via and its - // solution was reported by Emsy Chan by private mail on - // 2015-06-17. According to Robert van Ommen (2015-11-30), the - // same fix is required for Agfa Impax. This was generalized for - // generic manufacturer since it seems to affect PhilipsADW, - // GEWAServer as well: - // https://bitbucket.org/sjodogne/orthanc/issues/31/ - - switch (manufacturer) - { - case ModalityManufacturer_GenericNoWildcardInDates: - case ModalityManufacturer_GenericNoUniversalWildcard: - { - std::unique_ptr fix(fields.Clone()); - - std::set tags; - fix->GetTags(tags); - - for (std::set::const_iterator it = tags.begin(); it != tags.end(); ++it) - { - // Replace a "*" wildcard query by an empty query ("") for - // "date" or "all" value representations depending on the - // type of manufacturer. - if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard || - (manufacturer == ModalityManufacturer_GenericNoWildcardInDates && - FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date)) - { - const DicomValue* value = fix->TestAndGetValue(*it); - - if (value != NULL && - !value->IsNull() && - value->GetContent() == "*") - { - fix->SetValue(*it, "", false); - } - } - } - - return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */); - } - - default: - return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */); - } - } - - - static void ExecuteFind(DicomFindAnswers& answers, - T_ASC_Association* association, - DcmDataset* dataset, - const char* sopClass, - bool isWorklist, - const char* level, - uint32_t dimseTimeout, - const std::string& remoteAet) - { - assert(isWorklist ^ (level != NULL)); - - FindPayload payload; - payload.answers = &answers; - payload.level = level; - payload.isWorklist = isWorklist; - - // Figure out which of the accepted presentation contexts should be used - int presID = ASC_findAcceptedPresentationContextID(association, sopClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_DicomFindUnavailable, - "Remote AET is " + remoteAet); - } - - T_DIMSE_C_FindRQ request; - memset(&request, 0, sizeof(request)); - request.MessageID = association->nextMsgID++; - strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); - request.Priority = DIMSE_PRIORITY_MEDIUM; - request.DataSetType = DIMSE_DATASET_PRESENT; - - T_DIMSE_C_FindRSP response; - DcmDataset* statusDetail = NULL; - -#if DCMTK_VERSION_NUMBER >= 364 - int responseCount; -#endif - - OFCondition cond = DIMSE_findUser(association, presID, &request, dataset, -#if DCMTK_VERSION_NUMBER >= 364 - responseCount, -#endif - FindCallback, &payload, - /*opt_blockMode*/ (dimseTimeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ dimseTimeout, - &response, &statusDetail); - - if (statusDetail) - { - delete statusDetail; - } - - Check(cond, remoteAet, "C-FIND"); - - - /** - * New in Orthanc 1.6.0: Deal with failures during C-FIND. - * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1 - **/ - - if (response.DimseStatus != 0x0000 && // Success - response.DimseStatus != 0xFF00 && // Pending - Matches are continuing - response.DimseStatus != 0xFF01) // Pending - Matches are continuing - { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - - if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess) - { - throw OrthancException(ErrorCode_NetworkProtocol, - HttpStatus_422_UnprocessableEntity, - "C-FIND SCU to AET \"" + remoteAet + - "\" has failed with DIMSE status 0x" + buf + - " (unable to process - invalid query ?)" - ); - } - else - { - throw OrthancException(ErrorCode_NetworkProtocol, - "C-FIND SCU to AET \"" + remoteAet + - "\" has failed with DIMSE status 0x" + buf); - } - } - - } - - - void DicomUserConnection::Find(DicomFindAnswers& result, - ResourceType level, - const DicomMap& originalFields, - bool normalize) - { - CheckIsOpen(); - - std::unique_ptr query; - - if (normalize) - { - DicomMap fields; - NormalizeFindQuery(fields, level, originalFields); - query.reset(ConvertQueryFields(fields, manufacturer_)); - } - else - { - query.reset(new ParsedDicomFile(originalFields, - GetDefaultDicomEncoding(), - false /* be strict */)); - } - - DcmDataset* dataset = query->GetDcmtkObject().getDataset(); - - const char* clevel = NULL; - const char* sopClass = NULL; - - switch (level) - { - case ResourceType_Patient: - clevel = "PATIENT"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); - sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; - break; - - case ResourceType_Study: - clevel = "STUDY"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); - sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - break; - - case ResourceType_Series: - clevel = "SERIES"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); - sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - break; - - case ResourceType_Instance: - clevel = "IMAGE"; - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); - sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - - const char* universal; - if (manufacturer_ == ModalityManufacturer_GE) - { - universal = "*"; - } - else - { - universal = ""; - } - - - // Add the expected tags for this query level. - // WARNING: Do not reorder or add "break" in this switch-case! - switch (level) - { - case ResourceType_Instance: - if (!dataset->tagExists(DCM_SOPInstanceUID)) - { - DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal); - } - - case ResourceType_Series: - if (!dataset->tagExists(DCM_SeriesInstanceUID)) - { - DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal); - } - - case ResourceType_Study: - if (!dataset->tagExists(DCM_AccessionNumber)) - { - DU_putStringDOElement(dataset, DCM_AccessionNumber, universal); - } - - if (!dataset->tagExists(DCM_StudyInstanceUID)) - { - DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal); - } - - case ResourceType_Patient: - if (!dataset->tagExists(DCM_PatientID)) - { - DU_putStringDOElement(dataset, DCM_PatientID, universal); - } - - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - assert(clevel != NULL && sopClass != NULL); - ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, false, clevel, - pimpl_->dimseTimeout_, remoteAet_); - } - - - void DicomUserConnection::MoveInternal(const std::string& targetAet, - ResourceType level, - const DicomMap& fields) - { - CheckIsOpen(); - - std::unique_ptr query(ConvertQueryFields(fields, manufacturer_)); - DcmDataset* dataset = query->GetDcmtkObject().getDataset(); - - const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; - switch (level) - { - case ResourceType_Patient: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); - break; - - case ResourceType_Study: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); - break; - - case ResourceType_Series: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); - break; - - case ResourceType_Instance: - DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); - break; - - default: - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - // Figure out which of the accepted presentation contexts should be used - int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_DicomMoveUnavailable, - "Remote AET is " + remoteAet_); - } - - T_DIMSE_C_MoveRQ request; - memset(&request, 0, sizeof(request)); - request.MessageID = pimpl_->assoc_->nextMsgID++; - strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); - request.Priority = DIMSE_PRIORITY_MEDIUM; - request.DataSetType = DIMSE_DATASET_PRESENT; - strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN); - - T_DIMSE_C_MoveRSP response; - DcmDataset* statusDetail = NULL; - DcmDataset* responseIdentifiers = NULL; - OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset, - NULL, NULL, - /*opt_blockMode*/ (pimpl_->dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, - pimpl_->net_, NULL, NULL, - &response, &statusDetail, &responseIdentifiers); - - if (statusDetail) - { - delete statusDetail; - } - - if (responseIdentifiers) - { - delete responseIdentifiers; - } - - Check(cond, remoteAet_, "C-MOVE"); - - - /** - * New in Orthanc 1.6.0: Deal with failures during C-MOVE. - * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2 - **/ - - if (response.DimseStatus != 0x0000 && // Success - response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing - { - char buf[16]; - sprintf(buf, "%04X", response.DimseStatus); - - if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess) - { - throw OrthancException(ErrorCode_NetworkProtocol, - HttpStatus_422_UnprocessableEntity, - "C-MOVE SCU to AET \"" + remoteAet_ + - "\" has failed with DIMSE status 0x" + buf + - " (unable to process - resource not found ?)" - ); - } - else - { - throw OrthancException(ErrorCode_NetworkProtocol, - "C-MOVE SCU to AET \"" + remoteAet_ + - "\" has failed with DIMSE status 0x" + buf); - } - } - } - - - void DicomUserConnection::ResetStorageSOPClasses() - { - CheckStorageSOPClassesInvariant(); - - storageSOPClasses_.clear(); - defaultStorageSOPClasses_.clear(); - - // Copy the short list of storage SOP classes from DCMTK, making - // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**). - - std::set uncommon; - uncommon.insert(UID_BlendingSoftcopyPresentationStateStorage); - uncommon.insert(UID_GrayscaleSoftcopyPresentationStateStorage); - uncommon.insert(UID_ColorSoftcopyPresentationStateStorage); - uncommon.insert(UID_PseudoColorSoftcopyPresentationStateStorage); - uncommon.insert(UID_XAXRFGrayscaleSoftcopyPresentationStateStorage); - - // Add the storage syntaxes for C-STORE - for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs - 1; i++) - { - if (uncommon.find(dcmShortSCUStorageSOPClassUIDs[i]) == uncommon.end()) - { - defaultStorageSOPClasses_.insert(dcmShortSCUStorageSOPClassUIDs[i]); - } - } - - CheckStorageSOPClassesInvariant(); - } - - - void DicomUserConnection::DefaultSetup() - { - preferredTransferSyntax_ = DEFAULT_PREFERRED_TRANSFER_SYNTAX; - localAet_ = "STORESCU"; - remoteAet_ = "ANY-SCP"; - remoteHost_ = "127.0.0.1"; - remotePort_ = 104; - manufacturer_ = ModalityManufacturer_Generic; - - SetTimeout(defaultTimeout_); - pimpl_->net_ = NULL; - pimpl_->params_ = NULL; - pimpl_->assoc_ = NULL; - - // SOP classes for C-ECHO, C-FIND and C-MOVE (**) - reservedStorageSOPClasses_.push_back(UID_VerificationSOPClass); - reservedStorageSOPClasses_.push_back(UID_FINDPatientRootQueryRetrieveInformationModel); - reservedStorageSOPClasses_.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); - reservedStorageSOPClasses_.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); - reservedStorageSOPClasses_.push_back(UID_FINDModalityWorklistInformationModel); - - ResetStorageSOPClasses(); - } - - - DicomUserConnection::DicomUserConnection() : - pimpl_(new PImpl) - { - DefaultSetup(); - } - - - DicomUserConnection::DicomUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote) : - pimpl_(new PImpl) - { - DefaultSetup(); - SetLocalApplicationEntityTitle(localAet); - SetRemoteModality(remote); - } - - - DicomUserConnection::~DicomUserConnection() - { - Close(); - } - - - void DicomUserConnection::SetRemoteModality(const RemoteModalityParameters& parameters) - { - SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); - SetRemoteHost(parameters.GetHost()); - SetRemotePort(parameters.GetPortNumber()); - SetRemoteManufacturer(parameters.GetManufacturer()); - } - - - void DicomUserConnection::SetLocalApplicationEntityTitle(const std::string& aet) - { - if (localAet_ != aet) - { - Close(); - localAet_ = aet; - } - } - - void DicomUserConnection::SetRemoteApplicationEntityTitle(const std::string& aet) - { - if (remoteAet_ != aet) - { - Close(); - remoteAet_ = aet; - } - } - - void DicomUserConnection::SetRemoteManufacturer(ModalityManufacturer manufacturer) - { - if (manufacturer_ != manufacturer) - { - Close(); - manufacturer_ = manufacturer; - } - } - - void DicomUserConnection::ResetPreferredTransferSyntax() - { - SetPreferredTransferSyntax(DEFAULT_PREFERRED_TRANSFER_SYNTAX); - } - - void DicomUserConnection::SetPreferredTransferSyntax(const std::string& preferredTransferSyntax) - { - if (preferredTransferSyntax_ != preferredTransferSyntax) - { - Close(); - preferredTransferSyntax_ = preferredTransferSyntax; - } - } - - - void DicomUserConnection::SetRemoteHost(const std::string& host) - { - if (remoteHost_ != host) - { - if (host.size() > HOST_NAME_MAX - 10) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Invalid host name (too long): " + host); - } - - Close(); - remoteHost_ = host; - } - } - - void DicomUserConnection::SetRemotePort(uint16_t port) - { - if (remotePort_ != port) - { - Close(); - remotePort_ = port; - } - } - - void DicomUserConnection::OpenInternal(Mode mode) - { - if (IsOpen()) - { - // Don't reopen the connection - return; - } - - LOG(INFO) << "Opening a DICOM SCU connection from AET \"" << GetLocalApplicationEntityTitle() - << "\" to AET \"" << GetRemoteApplicationEntityTitle() << "\" on host " - << GetRemoteHost() << ":" << GetRemotePort() - << " (manufacturer: " << EnumerationToString(GetRemoteManufacturer()) << ")"; - - Check(ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ pimpl_->acseTimeout_, &pimpl_->net_), remoteAet_, "connecting"); - Check(ASC_createAssociationParameters(&pimpl_->params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU), remoteAet_, "connecting"); - - // Set this application's title and the called application's title in the params - Check(ASC_setAPTitles(pimpl_->params_, localAet_.c_str(), remoteAet_.c_str(), NULL), - remoteAet_, "connecting"); - - // Set the network addresses of the local and remote entities - char localHost[HOST_NAME_MAX]; - gethostname(localHost, HOST_NAME_MAX - 1); - - char remoteHostAndPort[HOST_NAME_MAX]; - -#ifdef _MSC_VER - _snprintf -#else - snprintf -#endif - (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", remoteHost_.c_str(), remotePort_); - - Check(ASC_setPresentationAddresses(pimpl_->params_, localHost, remoteHostAndPort), - remoteAet_, "connecting"); - - // Set various options - Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false), - remoteAet_, "connecting"); - - SetupPresentationContexts(mode, preferredTransferSyntax_); - - // Do the association - Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_), - remoteAet_, "connecting"); - - if (ASC_countAcceptedPresentationContexts(pimpl_->params_) == 0) - { - throw OrthancException(ErrorCode_NoPresentationContext, - "Unable to negotiate a presentation context with AET " + - remoteAet_); - } - } - - void DicomUserConnection::Close() - { - if (pimpl_->assoc_ != NULL) - { - ASC_releaseAssociation(pimpl_->assoc_); - ASC_destroyAssociation(&pimpl_->assoc_); - pimpl_->assoc_ = NULL; - pimpl_->params_ = NULL; - } - else - { - if (pimpl_->params_ != NULL) - { - ASC_destroyAssociationParameters(&pimpl_->params_); - pimpl_->params_ = NULL; - } - } - - if (pimpl_->net_ != NULL) - { - ASC_dropNetwork(&pimpl_->net_); - pimpl_->net_ = NULL; - } - } - - bool DicomUserConnection::IsOpen() const - { - return pimpl_->IsOpen(); - } - - void DicomUserConnection::Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const void* buffer, - size_t size, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - // Prepare an input stream for the memory buffer - DcmInputBufferStream is; - if (size > 0) - is.setBuffer(buffer, size); - is.setEos(); - - pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); - } - - void DicomUserConnection::Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& buffer, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - if (buffer.size() > 0) - Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(), - moveOriginatorAET, moveOriginatorID); - else - Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID); - } - - void DicomUserConnection::StoreFile(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& path, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID) - { - // Prepare an input stream for the file - DcmInputFileStream is(path.c_str()); - pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID); - } - - bool DicomUserConnection::Echo() - { - CheckIsOpen(); - DIC_US status; - Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, - /*opt_blockMode*/ (pimpl_->dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), - /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, - &status, NULL), remoteAet_, "C-ECHO"); - return status == STATUS_Success; - } - - - static void TestAndCopyTag(DicomMap& result, - const DicomMap& source, - const DicomTag& tag) - { - if (!source.HasTag(tag)) - { - throw OrthancException(ErrorCode_BadRequest); - } - else - { - result.SetValue(tag, source.GetValue(tag)); - } - } - - - void DicomUserConnection::Move(const std::string& targetAet, - ResourceType level, - const DicomMap& findResult) - { - DicomMap move; - switch (level) - { - case ResourceType_Patient: - TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); - break; - - case ResourceType_Study: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - break; - - case ResourceType_Series: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); - break; - - case ResourceType_Instance: - TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); - TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - MoveInternal(targetAet, level, move); - } - - - void DicomUserConnection::Move(const std::string& targetAet, - const DicomMap& findResult) - { - if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) - { - throw OrthancException(ErrorCode_InternalError); - } - - const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); - ResourceType level = StringToResourceType(tmp.c_str()); - - Move(targetAet, level, findResult); - } - - - void DicomUserConnection::MovePatient(const std::string& targetAet, - const std::string& patientId) - { - DicomMap query; - query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false); - MoveInternal(targetAet, ResourceType_Patient, query); - } - - void DicomUserConnection::MoveStudy(const std::string& targetAet, - const std::string& studyUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - MoveInternal(targetAet, ResourceType_Study, query); - } - - void DicomUserConnection::MoveSeries(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); - MoveInternal(targetAet, ResourceType_Series, query); - } - - void DicomUserConnection::MoveInstance(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid, - const std::string& instanceUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); - query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false); - MoveInternal(targetAet, ResourceType_Instance, query); - } - - - void DicomUserConnection::SetTimeout(uint32_t seconds) - { - if (seconds == 0) - { - DisableTimeout(); - } - else - { - dcmConnectionTimeout.set(seconds); - pimpl_->dimseTimeout_ = seconds; - pimpl_->acseTimeout_ = seconds; // Timeout used during association negociation and ASC_releaseAssociation() - } - } - - - void DicomUserConnection::DisableTimeout() - { - /** - * Global timeout (seconds) for connecting to remote hosts. - * Default value is -1 which selects infinite timeout, i.e. blocking connect(). - */ - dcmConnectionTimeout.set(-1); - pimpl_->dimseTimeout_ = 0; - pimpl_->acseTimeout_ = 10; // Timeout used during association negociation and ASC_releaseAssociation() - } - - - void DicomUserConnection::CheckStorageSOPClassesInvariant() const - { - assert(storageSOPClasses_.size() + - defaultStorageSOPClasses_.size() + - reservedStorageSOPClasses_.size() <= MAXIMUM_STORAGE_SOP_CLASSES); - } - - void DicomUserConnection::AddStorageSOPClass(const char* sop) - { - CheckStorageSOPClassesInvariant(); - - if (storageSOPClasses_.find(sop) != storageSOPClasses_.end()) - { - // This storage SOP class is already explicitly registered. Do - // nothing. - return; - } - - if (defaultStorageSOPClasses_.find(sop) != defaultStorageSOPClasses_.end()) - { - // This storage SOP class is not explicitly registered, but is - // used by default. Just register it explicitly. - defaultStorageSOPClasses_.erase(sop); - storageSOPClasses_.insert(sop); - - CheckStorageSOPClassesInvariant(); - return; - } - - // This storage SOP class is neither explicitly, nor implicitly - // registered. Close the connection and register it explicitly. - - Close(); - - if (reservedStorageSOPClasses_.size() + - storageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) // (*) - { - // The maximum number of SOP classes is reached - ResetStorageSOPClasses(); - defaultStorageSOPClasses_.erase(sop); - } - else if (reservedStorageSOPClasses_.size() + storageSOPClasses_.size() + - defaultStorageSOPClasses_.size() >= MAXIMUM_STORAGE_SOP_CLASSES) - { - // Make room in the default storage syntaxes - assert(!defaultStorageSOPClasses_.empty()); // Necessarily true because condition (*) is false - defaultStorageSOPClasses_.erase(*defaultStorageSOPClasses_.rbegin()); - } - - // Explicitly register the new storage syntax - storageSOPClasses_.insert(sop); - - CheckStorageSOPClassesInvariant(); - } - - - void DicomUserConnection::FindWorklist(DicomFindAnswers& result, - ParsedDicomFile& query) - { - CheckIsOpen(); - - DcmDataset* dataset = query.GetDcmtkObject().getDataset(); - const char* sopClass = UID_FINDModalityWorklistInformationModel; - - ExecuteFind(result, pimpl_->assoc_, dataset, sopClass, true, - NULL, pimpl_->dimseTimeout_, remoteAet_); - } - - - void DicomUserConnection::SetDefaultTimeout(uint32_t seconds) - { - LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " - << seconds << " seconds (0 = no timeout)"; - defaultTimeout_ = seconds; - } - - - bool DicomUserConnection::IsSameAssociation(const std::string& localAet, - const RemoteModalityParameters& remote) const - { - return (localAet_ == localAet && - remoteAet_ == remote.GetApplicationEntityTitle() && - remoteHost_ == remote.GetHost() && - remotePort_ == remote.GetPortNumber() && - manufacturer_ == remote.GetManufacturer()); - } - - - static void FillSopSequence(DcmDataset& dataset, - const DcmTagKey& tag, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids, - const std::vector& failureReasons, - bool hasFailureReasons) - { - assert(sopClassUids.size() == sopInstanceUids.size() && - (hasFailureReasons ? - failureReasons.size() == sopClassUids.size() : - failureReasons.empty())); - - if (sopInstanceUids.empty()) - { - // Add an empty sequence - if (!dataset.insertEmptyElement(tag).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - } - else - { - for (size_t i = 0; i < sopClassUids.size(); i++) - { - std::unique_ptr item(new DcmItem); - if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() || - !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() || - (hasFailureReasons && - !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) || - !dataset.insertSequenceItem(tag, item.release()).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - } - } - } - - - - - void DicomUserConnection::ReportStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids, - const std::vector& failureReasons) - { - if (sopClassUids.size() != sopInstanceUids.size() || - sopClassUids.size() != failureReasons.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - if (IsOpen()) - { - Close(); - } - - std::vector successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids; - std::vector failedReasons; - - successSopClassUids.reserve(sopClassUids.size()); - successSopInstanceUids.reserve(sopClassUids.size()); - failedSopClassUids.reserve(sopClassUids.size()); - failedSopInstanceUids.reserve(sopClassUids.size()); - failedReasons.reserve(sopClassUids.size()); - - for (size_t i = 0; i < sopClassUids.size(); i++) - { - switch (failureReasons[i]) - { - case StorageCommitmentFailureReason_Success: - successSopClassUids.push_back(sopClassUids[i]); - successSopInstanceUids.push_back(sopInstanceUids[i]); - break; - - case StorageCommitmentFailureReason_ProcessingFailure: - case StorageCommitmentFailureReason_NoSuchObjectInstance: - case StorageCommitmentFailureReason_ResourceLimitation: - case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported: - case StorageCommitmentFailureReason_ClassInstanceConflict: - case StorageCommitmentFailureReason_DuplicateTransactionUID: - failedSopClassUids.push_back(sopClassUids[i]); - failedSopInstanceUids.push_back(sopInstanceUids[i]); - failedReasons.push_back(failureReasons[i]); - break; - - default: - { - char buf[16]; - sprintf(buf, "%04xH", failureReasons[i]); - throw OrthancException(ErrorCode_ParameterOutOfRange, - "Unsupported failure reason for storage commitment: " + std::string(buf)); - } - } - } - - try - { - OpenInternal(Mode_ReportStorageCommitment); - - /** - * N-EVENT-REPORT - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1 - * - * Status code: - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 - **/ - - /** - * Send the "EVENT_REPORT_RQ" request - **/ - - LOG(INFO) << "Reporting modality \"" << remoteAet_ - << "\" about storage commitment transaction: " << transactionUid - << " (" << successSopClassUids.size() << " successes, " - << failedSopClassUids.size() << " failures)"; - const DIC_US messageId = pimpl_->assoc_->nextMsgID++; - - { - T_DIMSE_Message message; - memset(&message, 0, sizeof(message)); - message.CommandField = DIMSE_N_EVENT_REPORT_RQ; - - T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ; - content.MessageID = messageId; - strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); - strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); - content.DataSetType = DIMSE_DATASET_PRESENT; - - DcmDataset dataset; - if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - - { - std::vector empty; - FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids, - successSopInstanceUids, empty, false); - } - - // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html - if (failedSopClassUids.empty()) - { - content.EventTypeID = 1; // "Storage Commitment Request Successful" - } - else - { - content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist" - - // Failure reason - // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 - FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids, - failedSopInstanceUids, failedReasons, true); - } - - int presID = ASC_findAcceptedPresentationContextID( - pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to send N-EVENT-REPORT request to AET: " + remoteAet_); - } - - if (!DIMSE_sendMessageUsingMemoryData( - pimpl_->assoc_, presID, &message, NULL /* status detail */, - &dataset, NULL /* callback */, NULL /* callback context */, - NULL /* commandSet */).good()) - { - throw OrthancException(ErrorCode_NetworkProtocol); - } - } - - /** - * Read the "EVENT_REPORT_RSP" response - **/ - - { - T_ASC_PresentationContextID presID = 0; - T_DIMSE_Message message; - - const int timeout = pimpl_->dimseTimeout_; - if (!DIMSE_receiveCommand(pimpl_->assoc_, - (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, - &presID, &message, NULL /* no statusDetail */).good() || - message.CommandField != DIMSE_N_EVENT_REPORT_RSP) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to read N-EVENT-REPORT response from AET: " + remoteAet_); - } - - const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP; - if (content.MessageIDBeingRespondedTo != messageId || - !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) || - !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) || - //(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc - std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || - std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || - content.DataSetType != DIMSE_DATASET_NULL) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Badly formatted N-EVENT-REPORT response from AET: " + remoteAet_); - } - - if (content.DimseStatus != 0 /* success */) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "The request cannot be handled by remote AET: " + remoteAet_); - } - } - - Close(); - } - catch (OrthancException&) - { - Close(); - throw; - } - } - - - - void DicomUserConnection::RequestStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids) - { - if (sopClassUids.size() != sopInstanceUids.size()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - for (size_t i = 0; i < sopClassUids.size(); i++) - { - if (sopClassUids[i].empty() || - sopInstanceUids[i].empty()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange, - "The SOP class/instance UIDs cannot be empty, found: \"" + - sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\""); - } - } - - if (transactionUid.size() < 5 || - transactionUid.substr(0, 5) != "2.25.") - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - if (IsOpen()) - { - Close(); - } - - try - { - OpenInternal(Mode_RequestStorageCommitment); - - /** - * N-ACTION - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4 - * - * Status code: - * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 - **/ - - /** - * Send the "N_ACTION_RQ" request - **/ - - LOG(INFO) << "Request to modality \"" << remoteAet_ - << "\" about storage commitment for " << sopClassUids.size() - << " instances, with transaction UID: " << transactionUid; - const DIC_US messageId = pimpl_->assoc_->nextMsgID++; - - { - T_DIMSE_Message message; - memset(&message, 0, sizeof(message)); - message.CommandField = DIMSE_N_ACTION_RQ; - - T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ; - content.MessageID = messageId; - strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); - strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); - content.ActionTypeID = 1; // "Request Storage Commitment" - content.DataSetType = DIMSE_DATASET_PRESENT; - - DcmDataset dataset; - if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) - { - throw OrthancException(ErrorCode_InternalError); - } - - { - std::vector empty; - FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false); - } - - int presID = ASC_findAcceptedPresentationContextID( - pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass); - if (presID == 0) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to send N-ACTION request to AET: " + remoteAet_); - } - - if (!DIMSE_sendMessageUsingMemoryData( - pimpl_->assoc_, presID, &message, NULL /* status detail */, - &dataset, NULL /* callback */, NULL /* callback context */, - NULL /* commandSet */).good()) - { - throw OrthancException(ErrorCode_NetworkProtocol); - } - } - - /** - * Read the "N_ACTION_RSP" response - **/ - - { - T_ASC_PresentationContextID presID = 0; - T_DIMSE_Message message; - - const int timeout = pimpl_->dimseTimeout_; - if (!DIMSE_receiveCommand(pimpl_->assoc_, - (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout, - &presID, &message, NULL /* no statusDetail */).good() || - message.CommandField != DIMSE_N_ACTION_RSP) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Unable to read N-ACTION response from AET: " + remoteAet_); - } - - const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP; - if (content.MessageIDBeingRespondedTo != messageId || - !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) || - !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) || - //(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc - std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || - std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || - content.DataSetType != DIMSE_DATASET_NULL) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "Badly formatted N-ACTION response from AET: " + remoteAet_); - } - - if (content.DimseStatus != 0 /* success */) - { - throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " - "The request cannot be handled by remote AET: " + remoteAet_); - } - } - - Close(); - } - catch (OrthancException&) - { - Close(); - throw; - } - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/DicomUserConnection.h --- a/Core/DicomNetworking/DicomUserConnection.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,253 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1 -# error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1 -#endif - -#include "DicomFindAnswers.h" -#include "../Enumerations.h" -#include "RemoteModalityParameters.h" - -#include -#include -#include -#include - -namespace Orthanc -{ - class DicomUserConnection : public boost::noncopyable - { - private: - struct PImpl; - boost::shared_ptr pimpl_; - - enum Mode - { - Mode_Generic, - Mode_ReportStorageCommitment, - Mode_RequestStorageCommitment - }; - - // Connection parameters - std::string preferredTransferSyntax_; - std::string localAet_; - std::string remoteAet_; - std::string remoteHost_; - uint16_t remotePort_; - ModalityManufacturer manufacturer_; - std::set storageSOPClasses_; - std::list reservedStorageSOPClasses_; - std::set defaultStorageSOPClasses_; - - void CheckIsOpen() const; - - void SetupPresentationContexts(Mode mode, - const std::string& preferredTransferSyntax); - - void MoveInternal(const std::string& targetAet, - ResourceType level, - const DicomMap& fields); - - void ResetStorageSOPClasses(); - - void CheckStorageSOPClassesInvariant() const; - - void DefaultSetup(); - - void OpenInternal(Mode mode); - - public: - DicomUserConnection(); - - ~DicomUserConnection(); - - // This constructor corresponds to behavior of the old class - // "ReusableDicomUserConnection", without the call to "Open()" - DicomUserConnection(const std::string& localAet, - const RemoteModalityParameters& remote); - - void SetRemoteModality(const RemoteModalityParameters& parameters); - - void SetLocalApplicationEntityTitle(const std::string& aet); - - const std::string& GetLocalApplicationEntityTitle() const - { - return localAet_; - } - - void SetRemoteApplicationEntityTitle(const std::string& aet); - - const std::string& GetRemoteApplicationEntityTitle() const - { - return remoteAet_; - } - - void SetRemoteHost(const std::string& host); - - const std::string& GetRemoteHost() const - { - return remoteHost_; - } - - void SetRemotePort(uint16_t port); - - uint16_t GetRemotePort() const - { - return remotePort_; - } - - void SetRemoteManufacturer(ModalityManufacturer manufacturer); - - ModalityManufacturer GetRemoteManufacturer() const - { - return manufacturer_; - } - - void ResetPreferredTransferSyntax(); - - void SetPreferredTransferSyntax(const std::string& preferredTransferSyntax); - - const std::string& GetPreferredTransferSyntax() const - { - return preferredTransferSyntax_; - } - - void AddStorageSOPClass(const char* sop); - - void Open() - { - OpenInternal(Mode_Generic); - } - - void Close(); - - bool IsOpen() const; - - bool Echo(); - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const void* buffer, - size_t size, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const void* buffer, - size_t size) - { - Store(sopClassUid, sopInstanceUid, buffer, size, "", 0); // Not a C-Move - } - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& buffer, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void Store(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& buffer) - { - Store(sopClassUid, sopInstanceUid, buffer, "", 0); // Not a C-Move - } - - void StoreFile(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& path, - const std::string& moveOriginatorAET, - uint16_t moveOriginatorID); - - void StoreFile(std::string& sopClassUid /* out */, - std::string& sopInstanceUid /* out */, - const std::string& path) - { - StoreFile(sopClassUid, sopInstanceUid, path, "", 0); // Not a C-Move - } - - void Find(DicomFindAnswers& result, - ResourceType level, - const DicomMap& fields, - bool normalize); // Whether to normalize the DICOM query - - void Move(const std::string& targetAet, - ResourceType level, - const DicomMap& findResult); - - void Move(const std::string& targetAet, - const DicomMap& findResult); - - void MovePatient(const std::string& targetAet, - const std::string& patientId); - - void MoveStudy(const std::string& targetAet, - const std::string& studyUid); - - void MoveSeries(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid); - - void MoveInstance(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid, - const std::string& instanceUid); - - void SetTimeout(uint32_t seconds); - - void DisableTimeout(); - - void FindWorklist(DicomFindAnswers& result, - ParsedDicomFile& query); - - static void SetDefaultTimeout(uint32_t seconds); - - bool IsSameAssociation(const std::string& localAet, - const RemoteModalityParameters& remote) const; - - void ReportStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids, - const std::vector& failureReasons); - - // transactionUid: To be generated by Toolbox::GenerateDicomPrivateUniqueIdentifier() - void RequestStorageCommitment( - const std::string& transactionUid, - const std::vector& sopClassUids, - const std::vector& sopInstanceUids); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/RemoteModalityParameters.cpp --- a/Core/DicomNetworking/RemoteModalityParameters.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.cpp Wed May 20 16:42:44 2020 +0200 @@ -51,6 +51,7 @@ static const char* KEY_ALLOW_N_ACTION = "AllowNAction"; static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport"; static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment"; +static const char* KEY_ALLOW_TRANSCODING = "AllowTranscoding"; static const char* KEY_HOST = "Host"; static const char* KEY_MANUFACTURER = "Manufacturer"; static const char* KEY_PORT = "Port"; @@ -71,6 +72,7 @@ allowGet_ = true; allowNAction_ = true; // For storage commitment allowNEventReport_ = true; // For storage commitment + allowTranscoding_ = true; } @@ -233,6 +235,11 @@ allowNAction_ = allow; allowNEventReport_ = allow; } + + if (serialized.isMember(KEY_ALLOW_TRANSCODING)) + { + allowTranscoding_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_TRANSCODING); + } } @@ -314,7 +321,8 @@ !allowGet_ || !allowMove_ || !allowNAction_ || - !allowNEventReport_); + !allowNEventReport_ || + !allowTranscoding_); } @@ -336,6 +344,7 @@ target[KEY_ALLOW_MOVE] = allowMove_; target[KEY_ALLOW_N_ACTION] = allowNAction_; target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_; + target[KEY_ALLOW_TRANSCODING] = allowTranscoding_; } else { diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/RemoteModalityParameters.h --- a/Core/DicomNetworking/RemoteModalityParameters.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/RemoteModalityParameters.h Wed May 20 16:42:44 2020 +0200 @@ -55,6 +55,7 @@ bool allowGet_; bool allowNAction_; bool allowNEventReport_; + bool allowTranscoding_; void Clear(); @@ -131,5 +132,15 @@ void Serialize(Json::Value& target, bool forceAdvancedFormat) const; + + bool IsTranscodingAllowed() const + { + return allowTranscoding_; + } + + void SetTranscodingAllowed(bool allowed) + { + allowTranscoding_ = allowed; + } }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/TimeoutDicomConnectionManager.cpp --- a/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Wed May 20 16:42:44 2020 +0200 @@ -62,7 +62,7 @@ } - DicomUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection() + DicomStoreUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection() { if (that_.connection_.get() == NULL) { @@ -87,10 +87,12 @@ void TimeoutDicomConnectionManager::OpenInternal(const std::string& localAet, const RemoteModalityParameters& remote) { + DicomAssociationParameters other(localAet, remote); + if (connection_.get() == NULL || - !connection_->IsSameAssociation(localAet, remote)) + !connection_->GetParameters().IsEqual(other)) { - connection_.reset(new DicomUserConnection(localAet, remote)); + connection_.reset(new DicomStoreUserConnection(other)); } } @@ -101,7 +103,7 @@ if (connection_.get() != NULL) { LOG(INFO) << "Closing inactive DICOM association with modality: " - << connection_->GetRemoteApplicationEntityTitle(); + << connection_->GetParameters().GetRemoteModality().GetApplicationEntityTitle(); connection_.reset(NULL); } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomNetworking/TimeoutDicomConnectionManager.h --- a/Core/DicomNetworking/TimeoutDicomConnectionManager.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h Wed May 20 16:42:44 2020 +0200 @@ -43,7 +43,7 @@ #include "../Compatibility.h" -#include "DicomUserConnection.h" +#include "DicomStoreUserConnection.h" #include #include @@ -56,10 +56,10 @@ class TimeoutDicomConnectionManager : public boost::noncopyable { private: - boost::mutex mutex_; - std::unique_ptr connection_; - boost::posix_time::ptime lastUse_; - boost::posix_time::time_duration timeout_; + boost::mutex mutex_; + std::unique_ptr connection_; + boost::posix_time::ptime lastUse_; + boost::posix_time::time_duration timeout_; // Mutex must be locked void TouchInternal(); @@ -85,7 +85,7 @@ ~Lock(); - DicomUserConnection& GetConnection(); + DicomStoreUserConnection& GetConnection(); }; TimeoutDicomConnectionManager() : diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/DcmtkTranscoder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DcmtkTranscoder.cpp Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,330 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "DcmtkTranscoder.h" + + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG) +# error Macro ORTHANC_ENABLE_DCMTK_JPEG must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS) +# error Macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined +#endif + + +#include "FromDcmtkBridge.h" +#include "../OrthancException.h" + +#include +#include // for DJ_RPLossy +#include // for DJ_RPLossless +#include // for DJLSRepresentationParameter + + +namespace Orthanc +{ + static bool GetBitsStored(uint16_t& bitsStored, + DcmDataset& dataset) + { + return dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good(); + } + + + void DcmtkTranscoder::SetLossyQuality(unsigned int quality) + { + if (quality <= 0 || + quality > 100) + { + throw OrthancException( + ErrorCode_ParameterOutOfRange, + "The quality for lossy transcoding must be an integer between 1 and 100, received: " + + boost::lexical_cast(quality)); + } + else + { + LOG(INFO) << "Quality for lossy transcoding using DCMTK is set to: " << quality; + lossyQuality_ = quality; + } + } + + + bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */, + DcmFileFormat& dicom, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DicomTransferSyntax syntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, dicom)) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax"); + } + + uint16_t bitsStored; + bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset()); + + std::string sourceSopInstanceUid = IDicomTranscoder::GetSopInstanceUid(dicom); + + if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end()) + { + // No transcoding is needed + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL)) + { + selectedSyntax = DicomTransferSyntax_LittleEndianImplicit; + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL)) + { + selectedSyntax = DicomTransferSyntax_LittleEndianExplicit; + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL)) + { + selectedSyntax = DicomTransferSyntax_BigEndianExplicit; + return true; + } + + if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() && + FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL)) + { + selectedSyntax = DicomTransferSyntax_DeflatedLittleEndianExplicit; + return true; + } + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() && + allowNewSopInstanceUid && + (!hasBitsStored || bitsStored == 8)) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossy parameters(lossyQuality_); + + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, ¶meters)) + { + selectedSyntax = DicomTransferSyntax_JPEGProcess1; + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() && + allowNewSopInstanceUid && + (!hasBitsStored || bitsStored <= 12)) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossy parameters(lossyQuality_); + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, ¶meters)) + { + selectedSyntax = DicomTransferSyntax_JPEGProcess2_4; + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14) != allowedSyntaxes.end()) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossless parameters(6 /* opt_selection_value */, + 0 /* opt_point_transform */); + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, ¶meters)) + { + selectedSyntax = DicomTransferSyntax_JPEGProcess14; + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14SV1) != allowedSyntaxes.end()) + { + // Check out "dcmjpeg/apps/dcmcjpeg.cc" + DJ_RPLossless parameters(6 /* opt_selection_value */, + 0 /* opt_point_transform */); + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14SV1, ¶meters)) + { + selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1; + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + if (allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossless) != allowedSyntaxes.end()) + { + // Check out "dcmjpls/apps/dcmcjpls.cc" + DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */, + OFTrue /* opt_useLosslessProcess */); + + /** + * WARNING: This call results in a segmentation fault if using + * the DCMTK package 3.6.2 from Ubuntu 18.04. + **/ + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossless, ¶meters)) + { + selectedSyntax = DicomTransferSyntax_JPEGLSLossless; + return true; + } + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + if (allowNewSopInstanceUid && + allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossy) != allowedSyntaxes.end()) + { + // Check out "dcmjpls/apps/dcmcjpls.cc" + DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */, + OFFalse /* opt_useLosslessProcess */); + + /** + * WARNING: This call results in a segmentation fault if using + * the DCMTK package 3.6.2 from Ubuntu 18.04. + **/ + if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, ¶meters)) + { + selectedSyntax = DicomTransferSyntax_JPEGLSLossy; + return true; + } + } +#endif + + return false; + } + + + bool DcmtkTranscoder::IsSupported(DicomTransferSyntax syntax) + { + if (syntax == DicomTransferSyntax_LittleEndianImplicit || + syntax == DicomTransferSyntax_LittleEndianExplicit || + syntax == DicomTransferSyntax_BigEndianExplicit || + syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit) + { + return true; + } + +#if ORTHANC_ENABLE_DCMTK_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess1 || + syntax == DicomTransferSyntax_JPEGProcess2_4 || + syntax == DicomTransferSyntax_JPEGProcess14 || + syntax == DicomTransferSyntax_JPEGProcess14SV1) + { + return true; + } +#endif + +#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1 + if (syntax == DicomTransferSyntax_JPEGLSLossless || + syntax == DicomTransferSyntax_JPEGLSLossy) + { + return true; + } +#endif + + return false; + } + + + bool DcmtkTranscoder::Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + target.Clear(); + + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed())) + { + LOG(ERROR) << "Unsupport transfer syntax for transcoding"; + return false; + } + +#if !defined(NDEBUG) + const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed()); +#endif + + DicomTransferSyntax targetSyntax; + if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end()) + { + // No transcoding is needed + target.AcquireParsed(source); + target.AcquireBuffer(source); + return true; + } + else if (InplaceTranscode(targetSyntax, source.GetParsed(), + allowedSyntaxes, allowNewSopInstanceUid)) + { + // Sanity check + DicomTransferSyntax targetSyntax2; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, source.GetParsed()) && + targetSyntax == targetSyntax2 && + allowedSyntaxes.find(targetSyntax2) != allowedSyntaxes.end()) + { + target.AcquireParsed(source); + source.Clear(); + +#if !defined(NDEBUG) + // Only run the sanity check in debug mode + CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid, + allowedSyntaxes, allowNewSopInstanceUid); +#endif + + return true; + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + // Cannot transcode + return false; + } + } +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/DcmtkTranscoder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/DcmtkTranscoder.h Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,78 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_ENABLE_DCMTK_TRANSCODING) +# error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file +#endif + +#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1 +# error Transcoding is disabled, cannot compile this file +#endif + +#include "IDicomTranscoder.h" + +namespace Orthanc +{ + class DcmtkTranscoder : public IDicomTranscoder + { + private: + unsigned int lossyQuality_; + + bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */, + DcmFileFormat& dicom, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid); + + public: + DcmtkTranscoder() : + lossyQuality_(90) + { + } + + void SetLossyQuality(unsigned int quality); + + unsigned int GetLossyQuality() const + { + return lossyQuality_; + } + + static bool IsSupported(DicomTransferSyntax syntax); + + virtual bool Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + }; +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/FromDcmtkBridge.cpp --- a/Core/DicomParsing/FromDcmtkBridge.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.cpp Wed May 20 16:42:44 2020 +0200 @@ -121,6 +121,13 @@ #endif +#include +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 +# include +# include // include to support color images +#endif + + namespace Orthanc { static bool IsBinaryTag(const DcmTag& key) @@ -1198,51 +1205,30 @@ } } - bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, - DcmDataset& dataSet) + + + static bool SaveToMemoryBufferInternal(std::string& buffer, + DcmFileFormat& dicom, + E_TransferSyntax xfer) { - // Determine the transfer syntax which shall be used to write the - // information to the file. We always switch to the Little Endian - // syntax, with explicit length. - - // http://support.dcmtk.org/docs/dcxfer_8h-source.html - - - /** - * Note that up to Orthanc 0.7.1 (inclusive), the - * "EXS_LittleEndianExplicit" was always used to save the DICOM - * dataset into memory. We now keep the original transfer syntax - * (if available). - **/ - E_TransferSyntax xfer = dataSet.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - // No information about the original transfer syntax: This is - // most probably a DICOM dataset that was read from memory. - xfer = EXS_LittleEndianExplicit; - } - E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; - // Create the meta-header information - DcmFileFormat ff(&dataSet); - ff.validateMetaInfo(xfer); - ff.removeInvalidGroups(); - // Create a memory buffer with the proper size { - const uint32_t estimatedSize = ff.calcElementLength(xfer, encodingType); // (*) + const uint32_t estimatedSize = dicom.calcElementLength(xfer, encodingType); // (*) buffer.resize(estimatedSize); } DcmOutputBufferStream ob(&buffer[0], buffer.size()); // Fill the memory buffer with the meta-header and the dataset - ff.transferInit(); - OFCondition c = ff.write(ob, xfer, encodingType, NULL, - /*opt_groupLength*/ EGL_recalcGL, - /*opt_paddingType*/ EPD_withoutPadding); - ff.transferEnd(); + dicom.transferInit(); + OFCondition c = dicom.write(ob, xfer, encodingType, NULL, + /*opt_groupLength*/ EGL_recalcGL, + /*opt_paddingType*/ EPD_noChange, + /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0, + EWM_updateMeta /* creates new SOP instance UID on lossy */); + dicom.transferEnd(); if (c.good()) { @@ -1265,6 +1251,81 @@ return false; } } + + + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmDataset& dataSet) + { + // Determine the transfer syntax which shall be used to write the + // information to the file. If not possible, switch to the Little + // Endian syntax, with explicit length. + + // http://support.dcmtk.org/docs/dcxfer_8h-source.html + + + /** + * Note that up to Orthanc 0.7.1 (inclusive), the + * "EXS_LittleEndianExplicit" was always used to save the DICOM + * dataset into memory. We now keep the original transfer syntax + * (if available). + **/ + E_TransferSyntax xfer = dataSet.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + // No information about the original transfer syntax: This is + // most probably a DICOM dataset that was read from memory. + xfer = EXS_LittleEndianExplicit; + } + + // Create the meta-header information + DcmFileFormat ff(&dataSet); + ff.validateMetaInfo(xfer); + ff.removeInvalidGroups(); + + return SaveToMemoryBufferInternal(buffer, ff, xfer); + } + + + bool FromDcmtkBridge::Transcode(DcmFileFormat& dicom, + DicomTransferSyntax syntax, + const DcmRepresentationParameter* representation) + { + E_TransferSyntax xfer; + if (!LookupDcmtkTransferSyntax(xfer, syntax)) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + DicomTransferSyntax sourceSyntax; + bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom); + + if (!dicom.chooseRepresentation(xfer, representation).good() || + !dicom.canWriteXfer(xfer) || + !dicom.validateMetaInfo(xfer, EWM_updateMeta).good()) + { + return false; + } + else + { + dicom.removeInvalidGroups(); + + if (known) + { + LOG(INFO) << "Transcoded an image from transfer syntax " + << GetTransferSyntaxUid(sourceSyntax) << " to " + << GetTransferSyntaxUid(syntax); + } + else + { + LOG(INFO) << "Transcoded an image from unknown transfer syntax to " + << GetTransferSyntaxUid(syntax); + } + + return true; + } + } + } ValueRepresentation FromDcmtkBridge::LookupValueRepresentation(const DicomTag& tag) @@ -1659,7 +1720,7 @@ DcmTag key(tag.GetGroup(), tag.GetElement()); if (key.getEVR() != EVR_SQ) { - throw OrthancException(ErrorCode_BadParameterType); + throw OrthancException(ErrorCode_BadParameterType, "Bad Parameter type for tag " + tag.Format()); } DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key); @@ -1703,7 +1764,7 @@ } default: - throw OrthancException(ErrorCode_BadParameterType); + throw OrthancException(ErrorCode_BadParameterType, "Bad Parameter type for tag " + tag.Format()); } return element.release(); @@ -1719,9 +1780,13 @@ } DcmPixelData& pixelData = dynamic_cast(*element); + + E_TransferSyntax repType; + const DcmRepresentationParameter *repParam = NULL; + pixelData.getCurrentRepresentationKey(repType, repParam); + DcmPixelSequence* pixelSequence = NULL; - if (!pixelData.getEncapsulatedRepresentation - (dataset.getOriginalXfer(), NULL, pixelSequence).good()) + if (!pixelData.getEncapsulatedRepresentation(repType, repParam, pixelSequence).good()) { return NULL; } @@ -1974,25 +2039,6 @@ } - bool FromDcmtkBridge::LookupTransferSyntax(std::string& result, - DcmFileFormat& dicom) - { - const char* value = NULL; - - if (dicom.getMetaInfo() != NULL && - dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() && - value != NULL) - { - result.assign(value); - return true; - } - else - { - return false; - } - } - - #if ORTHANC_ENABLE_LUA == 1 void FromDcmtkBridge::ExecuteToDicom(DicomMap& target, LuaFunctionCall& call) @@ -2073,6 +2119,12 @@ DJEncoderRegistration::registerCodecs(); # endif #endif + + LOG(INFO) << "Registering RLE codecs in DCMTK"; + DcmRLEDecoderRegistration::registerCodecs(); +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + DcmRLEEncoderRegistration::registerCodecs(); +#endif } @@ -2093,6 +2145,11 @@ DJEncoderRegistration::cleanup(); # endif #endif + + DcmRLEDecoderRegistration::cleanup(); +#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 + DcmRLEEncoderRegistration::cleanup(); +#endif } @@ -2578,6 +2635,33 @@ Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding); ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions); } + + + + bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target, + DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DcmDataset& dataset = *dicom.getDataset(); + + E_TransferSyntax xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + dataset.updateOriginalXfer(); + xfer = dataset.getOriginalXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax of the DICOM instance"); + } + } + + return FromDcmtkBridge::LookupOrthancTransferSyntax(target, xfer); + } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/FromDcmtkBridge.h --- a/Core/DicomParsing/FromDcmtkBridge.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomParsing/FromDcmtkBridge.h Wed May 20 16:42:44 2020 +0200 @@ -205,6 +205,10 @@ static bool SaveToMemoryBuffer(std::string& buffer, DcmDataset& dataSet); + static bool Transcode(DcmFileFormat& dicom, + DicomTransferSyntax syntax, + const DcmRepresentationParameter* representation); + static ValueRepresentation Convert(DcmEVR vr); static ValueRepresentation LookupValueRepresentation(const DicomTag& tag); @@ -240,9 +244,6 @@ static void FromJson(DicomMap& values, const Json::Value& result); - static bool LookupTransferSyntax(std::string& result, - DcmFileFormat& dicom); - #if ORTHANC_ENABLE_LUA == 1 static void ExecuteToDicom(DicomMap& target, LuaFunctionCall& call); @@ -276,5 +277,8 @@ static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, E_TransferSyntax source); + + static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, + DcmFileFormat& dicom); }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/IDicomTranscoder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/IDicomTranscoder.cpp Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,438 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "IDicomTranscoder.h" + +#include "../OrthancException.h" +#include "FromDcmtkBridge.h" +#include "ParsedDicomFile.h" + +#include +#include + +namespace Orthanc +{ + IDicomTranscoder::TranscodingType IDicomTranscoder::GetTranscodingType(DicomTransferSyntax target, + DicomTransferSyntax source) + { + if (target == source) + { + return TranscodingType_Lossless; + } + else if (target == DicomTransferSyntax_LittleEndianImplicit || + target == DicomTransferSyntax_LittleEndianExplicit || + target == DicomTransferSyntax_BigEndianExplicit || + target == DicomTransferSyntax_DeflatedLittleEndianExplicit || + target == DicomTransferSyntax_JPEGProcess14 || + target == DicomTransferSyntax_JPEGProcess14SV1 || + target == DicomTransferSyntax_JPEGLSLossless || + target == DicomTransferSyntax_JPEG2000LosslessOnly || + target == DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly) + { + return TranscodingType_Lossless; + } + else if (target == DicomTransferSyntax_JPEGProcess1 || + target == DicomTransferSyntax_JPEGProcess2_4 || + target == DicomTransferSyntax_JPEGLSLossy || + target == DicomTransferSyntax_JPEG2000 || + target == DicomTransferSyntax_JPEG2000Multicomponent) + { + return TranscodingType_Lossy; + } + else + { + return TranscodingType_Unknown; + } + } + + + std::string IDicomTranscoder::GetSopInstanceUid(DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DcmDataset& dataset = *dicom.getDataset(); + + const char* v = NULL; + + if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() && + v != NULL) + { + return std::string(v); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID"); + } + } + + + void IDicomTranscoder::CheckTranscoding(IDicomTranscoder::DicomImage& transcoded, + DicomTransferSyntax sourceSyntax, + const std::string& sourceSopInstanceUid, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + DcmFileFormat& parsed = transcoded.GetParsed(); + + if (parsed.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string targetSopInstanceUid = GetSopInstanceUid(parsed); + + if (parsed.getDataset()->tagExists(DCM_PixelData)) + { + if (!allowNewSopInstanceUid && (targetSopInstanceUid != sourceSopInstanceUid)) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + if (targetSopInstanceUid != sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError, + "No pixel data: Transcoding must not change the SOP instance UID"); + } + } + + DicomTransferSyntax targetSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, parsed)) + { + return; // Unknown transfer syntax, cannot do further test + } + + if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end()) + { + // No transcoding should have happened + if (targetSopInstanceUid != sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError); + } + } + + if (allowedSyntaxes.find(targetSyntax) == allowedSyntaxes.end()) + { + throw OrthancException(ErrorCode_InternalError, "An incorrect output transfer syntax was chosen"); + } + + if (parsed.getDataset()->tagExists(DCM_PixelData)) + { + switch (GetTranscodingType(targetSyntax, sourceSyntax)) + { + case TranscodingType_Lossy: + if (targetSopInstanceUid == sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError); + } + break; + + case TranscodingType_Lossless: + if (targetSopInstanceUid != sourceSopInstanceUid) + { + throw OrthancException(ErrorCode_InternalError); + } + break; + + default: + break; + } + } + } + + + void IDicomTranscoder::DicomImage::Parse() + { + if (parsed_.get() != NULL) + { + // Already parsed + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (buffer_.get() != NULL) + { + if (isExternalBuffer_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer( + buffer_->empty() ? NULL : buffer_->c_str(), buffer_->size())); + + if (parsed_.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + } + else if (isExternalBuffer_) + { + parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(externalBuffer_, externalSize_)); + + if (parsed_.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + else + { + // No buffer is available + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void IDicomTranscoder::DicomImage::Serialize() + { + if (parsed_.get() == NULL || + buffer_.get() != NULL || + isExternalBuffer_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (parsed_->getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + buffer_.reset(new std::string); + FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, *parsed_->getDataset()); + } + } + + + IDicomTranscoder::DicomImage::DicomImage() : + isExternalBuffer_(false) + { + } + + + void IDicomTranscoder::DicomImage::Clear() + { + parsed_.reset(NULL); + buffer_.reset(NULL); + isExternalBuffer_ = false; + } + + + void IDicomTranscoder::DicomImage::AcquireParsed(ParsedDicomFile& parsed) + { + AcquireParsed(parsed.ReleaseDcmtkObject()); + } + + + void IDicomTranscoder::DicomImage::AcquireParsed(DcmFileFormat* parsed) + { + if (parsed == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (parsed->getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else if (parsed_.get() != NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parsed_.reset(parsed); + } + } + + + void IDicomTranscoder::DicomImage::AcquireParsed(DicomImage& other) + { + AcquireParsed(other.ReleaseParsed()); + } + + + void IDicomTranscoder::DicomImage::AcquireBuffer(std::string& buffer /* will be swapped */) + { + if (buffer_.get() != NULL || + isExternalBuffer_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + buffer_.reset(new std::string); + buffer_->swap(buffer); + } + } + + + void IDicomTranscoder::DicomImage::AcquireBuffer(DicomImage& other) + { + if (buffer_.get() != NULL || + isExternalBuffer_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (other.isExternalBuffer_) + { + assert(other.buffer_.get() == NULL); + isExternalBuffer_ = true; + externalBuffer_ = other.externalBuffer_; + externalSize_ = other.externalSize_; + } + else if (other.buffer_.get() != NULL) + { + buffer_.reset(other.buffer_.release()); + } + else + { + buffer_.reset(NULL); + } + } + + + void IDicomTranscoder::DicomImage::SetExternalBuffer(const void* buffer, + size_t size) + { + if (buffer_.get() != NULL || + isExternalBuffer_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + isExternalBuffer_ = true; + externalBuffer_ = buffer; + externalSize_ = size; + } + } + + + void IDicomTranscoder::DicomImage::SetExternalBuffer(const std::string& buffer) + { + SetExternalBuffer(buffer.empty() ? NULL : buffer.c_str(), buffer.size()); + } + + + DcmFileFormat& IDicomTranscoder::DicomImage::GetParsed() + { + if (parsed_.get() != NULL) + { + return *parsed_; + } + else if (buffer_.get() != NULL || + isExternalBuffer_) + { + Parse(); + return *parsed_; + } + else + { + throw OrthancException( + ErrorCode_BadSequenceOfCalls, + "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called"); + } + } + + + DcmFileFormat* IDicomTranscoder::DicomImage::ReleaseParsed() + { + if (parsed_.get() != NULL) + { + buffer_.reset(NULL); + return parsed_.release(); + } + else if (buffer_.get() != NULL || + isExternalBuffer_) + { + Parse(); + buffer_.reset(NULL); + return parsed_.release(); + } + else + { + throw OrthancException( + ErrorCode_BadSequenceOfCalls, + "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called"); + } + } + + + ParsedDicomFile* IDicomTranscoder::DicomImage::ReleaseAsParsedDicomFile() + { + return ParsedDicomFile::AcquireDcmtkObject(ReleaseParsed()); + } + + + const void* IDicomTranscoder::DicomImage::GetBufferData() + { + if (isExternalBuffer_) + { + assert(buffer_.get() == NULL); + return externalBuffer_; + } + else + { + if (buffer_.get() == NULL) + { + Serialize(); + } + + assert(buffer_.get() != NULL); + return buffer_->empty() ? NULL : buffer_->c_str(); + } + } + + + size_t IDicomTranscoder::DicomImage::GetBufferSize() + { + if (isExternalBuffer_) + { + assert(buffer_.get() == NULL); + return externalSize_; + } + else + { + if (buffer_.get() == NULL) + { + Serialize(); + } + + assert(buffer_.get() != NULL); + return buffer_->size(); + } + } +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/IDicomTranscoder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/IDicomTranscoder.h Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,131 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../Compatibility.h" +#include "../Enumerations.h" + +#include +#include + +class DcmFileFormat; + +namespace Orthanc +{ + /** + * WARNING: This class might be called from several threads at + * once. Make sure to implement proper locking. + **/ + + class ParsedDicomFile; + + class IDicomTranscoder : public boost::noncopyable + { + public: + class DicomImage : public boost::noncopyable + { + private: + std::unique_ptr parsed_; + std::unique_ptr buffer_; + bool isExternalBuffer_; + const void* externalBuffer_; + size_t externalSize_; + + void Parse(); + + void Serialize(); + + DcmFileFormat* ReleaseParsed(); + + public: + DicomImage(); + + void Clear(); + + // Calling this method will invalidate the "ParsedDicomFile" object + void AcquireParsed(ParsedDicomFile& parsed); + + void AcquireParsed(DcmFileFormat* parsed); + + void AcquireParsed(DicomImage& other); + + void AcquireBuffer(std::string& buffer /* will be swapped */); + + void AcquireBuffer(DicomImage& other); + + void SetExternalBuffer(const void* buffer, + size_t size); + + void SetExternalBuffer(const std::string& buffer); + + DcmFileFormat& GetParsed(); + + ParsedDicomFile* ReleaseAsParsedDicomFile(); + + const void* GetBufferData(); + + size_t GetBufferSize(); + }; + + + protected: + enum TranscodingType + { + TranscodingType_Lossy, + TranscodingType_Lossless, + TranscodingType_Unknown + }; + + static TranscodingType GetTranscodingType(DicomTransferSyntax target, + DicomTransferSyntax source); + + static void CheckTranscoding(DicomImage& transcoded, + DicomTransferSyntax sourceSyntax, + const std::string& sourceSopInstanceUid, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid); + + public: + virtual ~IDicomTranscoder() + { + } + + virtual bool Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) = 0; + + static std::string GetSopInstanceUid(DcmFileFormat& dicom); + }; +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/Internals/DicomImageDecoder.cpp --- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp Wed May 20 16:42:44 2020 +0200 @@ -676,7 +676,7 @@ ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset, unsigned int frame) { - E_TransferSyntax syntax = dataset.getOriginalXfer(); + E_TransferSyntax syntax = dataset.getCurrentXfer(); /** * Deal with uncompressed, raw images. @@ -810,7 +810,7 @@ **/ { - LOG(INFO) << "Decoding a compressed image by converting its transfer syntax to Little Endian"; + LOG(INFO) << "Trying to decode a compressed image by transcoding it to Little Endian Explicit"; std::unique_ptr converted(dynamic_cast(dataset.clone())); converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL); @@ -821,8 +821,18 @@ } } - throw OrthancException(ErrorCode_BadFileFormat, - "Cannot decode a DICOM image with the built-in decoder"); + DicomTransferSyntax s; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, dataset.getCurrentXfer())) + { + throw OrthancException(ErrorCode_NotImplemented, + "The built-in DCMTK decoder cannot decode some DICOM instance " + "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s))); + } + else + { + throw OrthancException(ErrorCode_NotImplemented, + "The built-in DCMTK decoder cannot decode some DICOM instance"); + } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/MemoryBufferTranscoder.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/MemoryBufferTranscoder.cpp Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,109 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "../PrecompiledHeaders.h" +#include "MemoryBufferTranscoder.h" + +#include "../OrthancException.h" +#include "FromDcmtkBridge.h" + +#if !defined(NDEBUG) // For debugging +# include "ParsedDicomFile.h" +#endif + +namespace Orthanc +{ + static void CheckTargetSyntax(const std::string& transcoded, + const std::set& allowedSyntaxes) + { +#if !defined(NDEBUG) + // Debug mode + ParsedDicomFile parsed(transcoded); + + std::string s; + DicomTransferSyntax a, b; + if (!parsed.LookupTransferSyntax(s) || + !FromDcmtkBridge::LookupOrthancTransferSyntax(a, parsed.GetDcmtkObject()) || + !LookupTransferSyntax(b, s) || + a != b || + allowedSyntaxes.find(a) == allowedSyntaxes.end()) + { + throw OrthancException( + ErrorCode_Plugin, + "DEBUG - The transcoding plugin has not written to one of the allowed transfer syntaxes"); + } +#endif + } + + + bool MemoryBufferTranscoder::Transcode(DicomImage& target, + DicomImage& source, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + target.Clear(); + +#if !defined(NDEBUG) + // Don't run this code in release mode, as it implies parsing the DICOM file + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed())) + { + LOG(ERROR) << "Unsupport transfer syntax for transcoding"; + return false; + } + + const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed()); +#endif + + std::string buffer; + if (TranscodeBuffer(buffer, source.GetBufferData(), source.GetBufferSize(), + allowedSyntaxes, allowNewSopInstanceUid)) + { + CheckTargetSyntax(buffer, allowedSyntaxes); // For debug only + + target.AcquireBuffer(buffer); + +#if !defined(NDEBUG) + // Only run the sanity check in debug mode + CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid, + allowedSyntaxes, allowNewSopInstanceUid); +#endif + + return true; + } + else + { + return false; + } + } +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/MemoryBufferTranscoder.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomParsing/MemoryBufferTranscoder.h Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,56 @@ +/** + * 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 "IDicomTranscoder.h" + +namespace Orthanc +{ + // This is the basis class for transcoding plugins + class MemoryBufferTranscoder : public IDicomTranscoder + { + protected: + virtual bool TranscodeBuffer(std::string& target, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) = 0; + + public: + virtual bool Transcode(DicomImage& target /* out */, + DicomImage& source, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + }; +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/ParsedDicomFile.cpp --- a/Core/DicomParsing/ParsedDicomFile.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.cpp Wed May 20 16:42:44 2020 +0200 @@ -455,8 +455,8 @@ void ParsedDicomFile::SendPathValue(RestApiOutput& output, const UriComponents& uri) { - DcmItem* dicom = pimpl_->file_->getDataset(); - E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer(); + DcmItem* dicom = GetDcmtkObject().getDataset(); + E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer(); // Special case: Accessing the pixel data if (uri.size() == 1 || @@ -516,7 +516,7 @@ InvalidateCache(); DcmTagKey key(tag.GetGroup(), tag.GetElement()); - DcmElement* element = pimpl_->file_->getDataset()->remove(key); + DcmElement* element = GetDcmtkObject().getDataset()->remove(key); if (element != NULL) { delete element; @@ -536,7 +536,7 @@ InvalidateCache(); - DcmItem* dicom = pimpl_->file_->getDataset(); + DcmItem* dicom = GetDcmtkObject().getDataset(); DcmTagKey key(tag.GetGroup(), tag.GetElement()); if (onlyIfExists && @@ -558,7 +558,7 @@ { InvalidateCache(); - DcmDataset& dataset = *pimpl_->file_->getDataset(); + DcmDataset& dataset = *GetDcmtkObject().getDataset(); // Loop over the dataset to detect its private tags typedef std::list Tags; @@ -629,7 +629,7 @@ return; } - if (pimpl_->file_->getDataset()->tagExists(ToDcmtkBridge::Convert(tag))) + if (GetDcmtkObject().getDataset()->tagExists(ToDcmtkBridge::Convert(tag))) { throw OrthancException(ErrorCode_AlreadyExistingTag); } @@ -650,7 +650,7 @@ bool hasCodeExtensions; Encoding encoding = DetectEncoding(hasCodeExtensions); std::unique_ptr element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator)); - InsertInternal(*pimpl_->file_->getDataset(), element.release()); + InsertInternal(*GetDcmtkObject().getDataset(), element.release()); } @@ -782,7 +782,7 @@ InvalidateCache(); - DcmDataset& dicom = *pimpl_->file_->getDataset(); + DcmDataset& dicom = *GetDcmtkObject().getDataset(); if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode)) { // Either the tag was previously existing (and now removed), or @@ -828,7 +828,7 @@ InvalidateCache(); - DcmDataset& dicom = *pimpl_->file_->getDataset(); + DcmDataset& dicom = *GetDcmtkObject().getDataset(); if (CanReplaceProceed(dicom, ToDcmtkBridge::Convert(tag), mode)) { // Either the tag was previously existing (and now removed), or @@ -867,9 +867,9 @@ void ParsedDicomFile::Answer(RestApiOutput& output) { std::string serialized; - if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *pimpl_->file_->getDataset())) + if (FromDcmtkBridge::SaveToMemoryBuffer(serialized, *GetDcmtkObject().getDataset())) { - output.AnswerBuffer(serialized, MimeType_Binary); + output.AnswerBuffer(serialized, MimeType_Dicom); } } #endif @@ -879,7 +879,7 @@ const DicomTag& tag) { DcmTagKey k(tag.GetGroup(), tag.GetElement()); - DcmDataset& dataset = *pimpl_->file_->getDataset(); + DcmDataset& dataset = *GetDcmtkObject().getDataset(); if (tag.IsPrivate() || FromDcmtkBridge::IsUnknownTag(tag) || @@ -970,7 +970,7 @@ void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer) { - FromDcmtkBridge::SaveToMemoryBuffer(buffer, *pimpl_->file_->getDataset()); + FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().getDataset()); } @@ -1004,6 +1004,7 @@ bool permissive) { pimpl_->file_.reset(new DcmFileFormat); + pimpl_->frameIndex_.reset(NULL); const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET); @@ -1091,7 +1092,7 @@ bool keepSopInstanceUid) : pimpl_(new PImpl) { - pimpl_->file_.reset(dynamic_cast(other.pimpl_->file_->clone())); + pimpl_->file_.reset(dynamic_cast(other.GetDcmtkObject().clone())); if (!keepSopInstanceUid) { @@ -1113,9 +1114,38 @@ } + ParsedDicomFile::ParsedDicomFile(DcmFileFormat* dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(dicom); // No cloning + } + + DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const { - return *pimpl_->file_.get(); + if (pimpl_->file_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "ReleaseDcmtkObject() was called"); + } + else + { + return *pimpl_->file_; + } + } + + + DcmFileFormat* ParsedDicomFile::ReleaseDcmtkObject() + { + if (pimpl_->file_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "ReleaseDcmtkObject() was called"); + } + else + { + pimpl_->frameIndex_.reset(NULL); + return pimpl_->file_.release(); + } } @@ -1348,7 +1378,7 @@ } } - if (!pimpl_->file_->getDataset()->insert(pixels.release(), false, false).good()) + if (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good()) { throw OrthancException(ErrorCode_InternalError); } @@ -1358,7 +1388,7 @@ Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const { return FromDcmtkBridge::DetectEncoding(hasCodeExtensions, - *pimpl_->file_->getDataset(), + *GetDcmtkObject().getDataset(), GetDefaultDicomEncoding()); } @@ -1382,7 +1412,7 @@ unsigned int maxStringLength) { std::set ignoreTagLength; - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), format, flags, maxStringLength, GetDefaultDicomEncoding(), ignoreTagLength); } @@ -1394,7 +1424,7 @@ unsigned int maxStringLength, const std::set& ignoreTagLength) { - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), format, flags, maxStringLength, GetDefaultDicomEncoding(), ignoreTagLength); } @@ -1403,28 +1433,28 @@ void ParsedDicomFile::DatasetToJson(Json::Value& target, const std::set& ignoreTagLength) { - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength); + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength); } void ParsedDicomFile::DatasetToJson(Json::Value& target) { const std::set ignoreTagLength; - FromDcmtkBridge::ExtractDicomAsJson(target, *pimpl_->file_->getDataset(), ignoreTagLength); + FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength); } void ParsedDicomFile::HeaderToJson(Json::Value& target, DicomToJsonFormat format) { - FromDcmtkBridge::ExtractHeaderAsJson(target, *pimpl_->file_->getMetaInfo(), format, DicomToJsonFlags_None, 0); + FromDcmtkBridge::ExtractHeaderAsJson(target, *GetDcmtkObject().getMetaInfo(), format, DicomToJsonFlags_None, 0); } bool ParsedDicomFile::HasTag(const DicomTag& tag) const { DcmTag key(tag.GetGroup(), tag.GetElement()); - return pimpl_->file_->getDataset()->tagExists(key); + return GetDcmtkObject().getDataset()->tagExists(key); } @@ -1466,7 +1496,7 @@ memcpy(bytes, pdf.c_str(), pdf.size()); DcmPolymorphOBOW* obj = element.release(); - result = pimpl_->file_->getDataset()->insert(obj); + result = GetDcmtkObject().getDataset()->insert(obj); if (!result.good()) { @@ -1558,13 +1588,13 @@ if (pimpl_->frameIndex_.get() == NULL) { assert(pimpl_->file_ != NULL && - pimpl_->file_->getDataset() != NULL); - pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_->getDataset())); + GetDcmtkObject().getDataset() != NULL); + pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObject().getDataset())); } pimpl_->frameIndex_->GetRawFrame(target, frameId); - E_TransferSyntax transferSyntax = pimpl_->file_->getDataset()->getOriginalXfer(); + E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer(); switch (transferSyntax) { case EXS_JPEGProcess1: @@ -1592,8 +1622,8 @@ unsigned int ParsedDicomFile::GetFramesCount() const { assert(pimpl_->file_ != NULL && - pimpl_->file_->getDataset() != NULL); - return DicomFrameIndex::GetFramesCount(*pimpl_->file_->getDataset()); + GetDcmtkObject().getDataset() != NULL); + return DicomFrameIndex::GetFramesCount(*GetDcmtkObject().getDataset()); } @@ -1605,27 +1635,56 @@ if (source != target) // Avoid unnecessary conversion { ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target)); - FromDcmtkBridge::ChangeStringEncoding(*pimpl_->file_->getDataset(), source, hasCodeExtensions, target); + FromDcmtkBridge::ChangeStringEncoding(*GetDcmtkObject().getDataset(), source, hasCodeExtensions, target); } } void ParsedDicomFile::ExtractDicomSummary(DicomMap& target) const { - FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset()); + FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset()); } void ParsedDicomFile::ExtractDicomSummary(DicomMap& target, const std::set& ignoreTagLength) const { - FromDcmtkBridge::ExtractDicomSummary(target, *pimpl_->file_->getDataset(), ignoreTagLength); + FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset(), ignoreTagLength); } bool ParsedDicomFile::LookupTransferSyntax(std::string& result) { - return FromDcmtkBridge::LookupTransferSyntax(result, *pimpl_->file_); +#if 0 + // This was the implementation in Orthanc <= 1.6.1 + + // TODO - Shouldn't "dataset.getCurrentXfer()" be used instead of + // using the meta header? + const char* value = NULL; + + if (GetDcmtkObject().getMetaInfo() != NULL && + GetDcmtkObject().getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() && + value != NULL) + { + result.assign(value); + return true; + } + else + { + return false; + } +#else + DicomTransferSyntax s; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, GetDcmtkObject())) + { + result.assign(GetTransferSyntaxUid(s)); + return true; + } + else + { + return false; + } +#endif } @@ -1634,7 +1693,7 @@ DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(), DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement()); - DcmDataset& dataset = *pimpl_->file_->getDataset(); + DcmDataset& dataset = *GetDcmtkObject().getDataset(); const char *c = NULL; if (dataset.findAndGetString(k, c).good() && @@ -1652,6 +1711,6 @@ void ParsedDicomFile::Apply(ITagVisitor& visitor) { - FromDcmtkBridge::Apply(*pimpl_->file_->getDataset(), visitor, GetDefaultDicomEncoding()); + FromDcmtkBridge::Apply(*GetDcmtkObject().getDataset(), visitor, GetDefaultDicomEncoding()); } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/DicomParsing/ParsedDicomFile.h --- a/Core/DicomParsing/ParsedDicomFile.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/DicomParsing/ParsedDicomFile.h Wed May 20 16:42:44 2020 +0200 @@ -102,6 +102,8 @@ bool EmbedContentInternal(const std::string& dataUriScheme); + ParsedDicomFile(DcmFileFormat* dicom); // This takes ownership (no clone) + public: ParsedDicomFile(bool createIdentifiers); // Create a minimal DICOM instance @@ -114,12 +116,20 @@ ParsedDicomFile(const std::string& content); - ParsedDicomFile(DcmDataset& dicom); + ParsedDicomFile(DcmDataset& dicom); // This clones the DCMTK object + + ParsedDicomFile(DcmFileFormat& dicom); // This clones the DCMTK object - ParsedDicomFile(DcmFileFormat& dicom); + static ParsedDicomFile* AcquireDcmtkObject(DcmFileFormat* dicom) // No clone here + { + return new ParsedDicomFile(dicom); + } DcmFileFormat& GetDcmtkObject() const; + // The "ParsedDicomFile" object cannot be used after calling this method + DcmFileFormat* ReleaseDcmtkObject(); + ParsedDicomFile* Clone(bool keepSopInstanceUid); #if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1 diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/Enumerations.h --- a/Core/Enumerations.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/Enumerations.h Wed May 20 16:42:44 2020 +0200 @@ -279,13 +279,13 @@ DicomTransferSyntax_JPEG2000Multicomponent /*!< JPEG 2000 part 2 multicomponent extensions (lossless or lossy) */, DicomTransferSyntax_JPIPReferenced /*!< JPIP Referenced */, DicomTransferSyntax_JPIPReferencedDeflate /*!< JPIP Referenced Deflate */, - DicomTransferSyntax_MPEG2MainProfileAtMainLevel /*!< MPEG2 Main Profile at Main Level */, - DicomTransferSyntax_MPEG2MainProfileAtHighLevel /*!< MPEG2 Main Profile at High Level */, - DicomTransferSyntax_MPEG4HighProfileLevel4_1 /*!< MPEG4 High Profile / Level 4.1 */, - DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1 /*!< MPEG4 BD-compatible High Profile / Level 4.1 */, - DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo /*!< MPEG4 High Profile / Level 4.2 For 2D Video */, - DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo /*!< MPEG4 High Profile / Level 4.2 For 3D Video */, - DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2 /*!< 1.2.840.10008.1.2.4.106 */, + DicomTransferSyntax_MPEG2MainProfileAtMainLevel /*!< MPEG2 Main Profile / Main Level */, + DicomTransferSyntax_MPEG2MainProfileAtHighLevel /*!< MPEG2 Main Profile / High Level */, + DicomTransferSyntax_MPEG4HighProfileLevel4_1 /*!< MPEG4 AVC/H.264 High Profile / Level 4.1 */, + DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1 /*!< MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1 */, + DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video */, + DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video */, + DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2 /*!< MPEG4 AVC/H.264 Stereo High Profile / Level 4.2 */, DicomTransferSyntax_HEVCMainProfileLevel5_1 /*!< HEVC/H.265 Main Profile / Level 5.1 */, DicomTransferSyntax_HEVCMain10ProfileLevel5_1 /*!< HEVC/H.265 Main 10 Profile / Level 5.1 */, DicomTransferSyntax_RLELossless /*!< RLE - Run Length Encoding (lossless) */, @@ -753,7 +753,7 @@ DicomAssociationRole_Scu, DicomAssociationRole_Scp }; - + /** * WARNING: Do not change the explicit values in the enumerations diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/SerializationToolbox.cpp --- a/Core/SerializationToolbox.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Core/SerializationToolbox.cpp Wed May 20 16:42:44 2020 +0200 @@ -98,6 +98,21 @@ } + int ReadInteger(const Json::Value& value, + const std::string& field, + int defaultValue) + { + if (value.isMember(field.c_str())) + { + return ReadInteger(value, field); + } + else + { + return defaultValue; + } + } + + unsigned int ReadUnsignedInteger(const Json::Value& value, const std::string& field) { diff -r fe0e4ef52a72 -r 6e14f2da7c7e Core/SerializationToolbox.h --- a/Core/SerializationToolbox.h Wed May 06 08:40:48 2020 +0200 +++ b/Core/SerializationToolbox.h Wed May 20 16:42:44 2020 +0200 @@ -49,6 +49,10 @@ int ReadInteger(const Json::Value& value, const std::string& field); + int ReadInteger(const Json::Value& value, + const std::string& field, + int defaultValue); + unsigned int ReadUnsignedInteger(const Json::Value& value, const std::string& field); diff -r fe0e4ef52a72 -r 6e14f2da7c7e LinuxCompilation.txt --- a/LinuxCompilation.txt Wed May 06 08:40:48 2020 +0200 +++ b/LinuxCompilation.txt Wed May 20 16:42:44 2020 +0200 @@ -44,7 +44,7 @@ directory ("rm -rf ~/OrthancBuild") after installing this package, then run cmake again. -Note 3- To build the documentation, you will have to install doxyen. +Note 3- To build the documentation, you will have to install doxygen. Use system-wide libraries under GNU/Linux diff -r fe0e4ef52a72 -r 6e14f2da7c7e NEWS --- a/NEWS Wed May 06 08:40:48 2020 +0200 +++ b/NEWS Wed May 20 16:42:44 2020 +0200 @@ -1,19 +1,65 @@ Pending changes in the mainline =============================== +General +------- + +* DICOM transcoding over the REST API +* Transcoding from compressed to uncompressed transfer syntaxes over DICOM + C-STORE SCU (if the remote modality doesn't support compressed syntaxes) +* New configuration options related to transcoding: + "TranscodeDicomProtocol", "BuiltinDecoderTranscoderOrder", + "IngestTranscoding" and "DicomLossyTranscodingQuality" + REST API -------- +* API version upgraded to 7 * Improved: - "/instances/../modify": it is now possible to "Keep" the "SOPInstanceUID". Note that it was already possible to "Replace" it. + - added "Timeout" parameter to every DICOM operation + - "/queries/.../answers/../retrieve": "TargetAet" not mandatory anymore + (defaults to the local AET) +* Changes: + - "/{patients|studies|series}/.../modify": New option "KeepSource" + - "/{patients|studies|series|instances}/.../modify": New option "Transcode" + - "/peers/{id}/store": New option "Transcode" + - ".../archive", ".../media", "/tools/create-media" and + "/tools/create-archive": New option "Transcode" + - "/ordered-slices": reverted the change introduced in 1.5.8 and go-back + to 1.5.7 behaviour. + +Plugins +------- + +* New functions in the SDK: + - OrthancPluginCreateDicomInstance() + - OrthancPluginCreateMemoryBuffer() + - OrthancPluginEncodeDicomWebJson2() + - OrthancPluginEncodeDicomWebXml2() + - OrthancPluginFreeDicomInstance() + - OrthancPluginGetInstanceAdvancedJson() + - OrthancPluginGetInstanceDecodedFrame() + - OrthancPluginGetInstanceDicomWebJson() + - OrthancPluginGetInstanceDicomWebXml() + - OrthancPluginGetInstanceFramesCount() + - OrthancPluginGetInstanceRawFrame() + - OrthancPluginRegisterTranscoderCallback() + - OrthancPluginSerializeDicomInstance() + - OrthancPluginTranscodeDicomInstance() +* "OrthancPluginDicomInstance" structure wrapped in "OrthancPluginCppWrapper.h" +* Allow concurrent calls to the custom image decoders provided by the plugins Maintenance ----------- +* Moved the GDCM sample plugin out of the Orthanc repository as a separate plugin * Fix missing body in "OrthancPluginHttpPost()" and "OrthancPluginHttpPut()" +* Fix issue #169 (TransferSyntaxUID change from Explicit to Implicit during C-STORE SCU) * Upgraded dependencies for static builds (notably on Windows and LSB): - openssl 1.1.1g +* Fix issue #179 (deadlock in Python plugins) Version 1.6.1 (2020-04-21) diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/DefaultDicomImageDecoder.h --- a/OrthancServer/DefaultDicomImageDecoder.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IDicomImageDecoder.h" -#include "../Core/DicomParsing/ParsedDicomFile.h" -#include "../Core/DicomParsing/Internals/DicomImageDecoder.h" - -namespace Orthanc -{ - class DefaultDicomImageDecoder : public IDicomImageDecoder - { - public: - virtual ImageAccessor* Decode(const void* dicom, - size_t size, - unsigned int frame) - { - ParsedDicomFile parsed(dicom, size); - return DicomImageDecoder::Decode(parsed, frame); - } - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/DicomInstanceToStore.cpp --- a/OrthancServer/DicomInstanceToStore.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/DicomInstanceToStore.cpp Wed May 20 16:42:44 2020 +0200 @@ -381,14 +381,14 @@ } - bool HasPixelData() + ParsedDicomFile& GetParsedDicomFile() { ComputeMissingInformation(); ParseDicomFile(); if (parsed_.HasContent()) { - return parsed_.GetContent().HasTag(DICOM_TAG_PIXEL_DATA); + return parsed_.GetContent(); } else { @@ -498,6 +498,11 @@ bool DicomInstanceToStore::HasPixelData() const { - return const_cast(*pimpl_).HasPixelData(); + return const_cast(*pimpl_).GetParsedDicomFile().HasTag(DICOM_TAG_PIXEL_DATA); + } + + ParsedDicomFile& DicomInstanceToStore::GetParsedDicomFile() const + { + return const_cast(*pimpl_).GetParsedDicomFile(); } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/DicomInstanceToStore.h --- a/OrthancServer/DicomInstanceToStore.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/DicomInstanceToStore.h Wed May 20 16:42:44 2020 +0200 @@ -92,5 +92,7 @@ DicomInstanceHasher& GetHasher(); bool HasPixelData() const; + + ParsedDicomFile& GetParsedDicomFile() const; }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/IServerListener.h --- a/OrthancServer/IServerListener.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/IServerListener.h Wed May 20 16:42:44 2020 +0200 @@ -48,7 +48,7 @@ } virtual void SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) = 0; virtual void SignalChange(const ServerIndexChange& change) = 0; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/LuaScripting.cpp --- a/OrthancServer/LuaScripting.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/LuaScripting.cpp Wed May 20 16:42:44 2020 +0200 @@ -588,7 +588,7 @@ } // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()" - return lock.AddStoreScuOperation(localAet, modality); + return lock.AddStoreScuOperation(context_, localAet, modality); } if (operation == "store-peer") @@ -821,7 +821,7 @@ void LuaScripting::SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) { Json::Value metadata = Json::objectValue; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/LuaScripting.h --- a/OrthancServer/LuaScripting.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/LuaScripting.h Wed May 20 16:42:44 2020 +0200 @@ -121,7 +121,7 @@ void Stop(); void SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags); void SignalChange(const ServerIndexChange& change); diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancConfiguration.cpp --- a/OrthancServer/OrthancConfiguration.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancConfiguration.cpp Wed May 20 16:42:44 2020 +0200 @@ -422,8 +422,8 @@ } - std::string OrthancConfiguration::GetStringParameter(const std::string& parameter, - const std::string& defaultValue) const + bool OrthancConfiguration::LookupStringParameter(std::string& target, + const std::string& parameter) const { if (json_.isMember(parameter)) { @@ -434,11 +434,27 @@ } else { - return json_[parameter].asString(); + target = json_[parameter].asString(); + return true; } } else { + return false; + } + } + + + std::string OrthancConfiguration::GetStringParameter(const std::string& parameter, + const std::string& defaultValue) const + { + std::string value; + if (LookupStringParameter(value, parameter)) + { + return value; + } + else + { return defaultValue; } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancConfiguration.h --- a/OrthancServer/OrthancConfiguration.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancConfiguration.h Wed May 20 16:42:44 2020 +0200 @@ -163,6 +163,9 @@ fontRegistry_.AddFromResource(resource); } + bool LookupStringParameter(std::string& target, + const std::string& parameter) const; + std::string GetStringParameter(const std::string& parameter, const std::string& defaultValue) const; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Wed May 20 16:42:44 2020 +0200 @@ -59,7 +59,7 @@ RemoteModalityParameters remote_; std::string originatorAet_; uint16_t originatorId_; - std::unique_ptr connection_; + std::unique_ptr connection_; public: SynchronousMove(ServerContext& context, @@ -113,11 +113,15 @@ if (connection_.get() == NULL) { - connection_.reset(new DicomUserConnection(localAet_, remote_)); + DicomAssociationParameters params(localAet_, remote_); + connection_.reset(new DicomStoreUserConnection(params)); } std::string sopClassUid, sopInstanceUid; // Unused - connection_->Store(sopClassUid, sopInstanceUid, dicom, originatorAet_, originatorId_); + + const void* data = dicom.empty() ? NULL : dicom.c_str(); + connection_->Store(sopClassUid, sopInstanceUid, data, dicom.size(), + true, originatorAet_, originatorId_); return Status_Success; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed May 20 16:42:44 2020 +0200 @@ -112,19 +112,48 @@ static void AnonymizeOrModifyInstance(DicomModification& modification, - RestApiPostCall& call) + RestApiPostCall& call, + bool transcode, + DicomTransferSyntax targetSyntax) { + ServerContext& context = OrthancRestApi::GetContext(call); std::string id = call.GetUriComponent("id", ""); std::unique_ptr modified; { - ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id); + ServerContext::DicomCacheLocker locker(context, id); modified.reset(locker.GetDicom().Clone(true)); } modification.Apply(*modified); - modified->Answer(call.GetOutput()); + + if (transcode) + { + IDicomTranscoder::DicomImage source; + source.AcquireParsed(*modified); // "modified" is invalid below this point + + IDicomTranscoder::DicomImage transcoded; + + std::set s; + s.insert(targetSyntax); + + if (context.Transcode(transcoded, source, s, true)) + { + call.GetOutput().AnswerBuffer(transcoded.GetBufferData(), + transcoded.GetBufferSize(), MimeType_Dicom); + } + else + { + throw OrthancException(ErrorCode_InternalError, + "Cannot transcode to transfer syntax: " + + std::string(GetTransferSyntaxUid(targetSyntax))); + } + } + else + { + modified->Answer(call.GetOutput()); + } } @@ -153,7 +182,26 @@ modification.SetLevel(ResourceType_Instance); } - AnonymizeOrModifyInstance(modification, call); + static const char* TRANSCODE = "Transcode"; + if (request.isMember(TRANSCODE)) + { + std::string s = SerializationToolbox::ReadString(request, TRANSCODE); + + DicomTransferSyntax syntax; + if (LookupTransferSyntax(syntax, s)) + { + AnonymizeOrModifyInstance(modification, call, true, syntax); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s); + } + } + else + { + AnonymizeOrModifyInstance(modification, call, false /* no transcoding */, + DicomTransferSyntax_LittleEndianImplicit /* unused */); + } } @@ -165,7 +213,19 @@ Json::Value request; ParseAnonymizationRequest(request, modification, call); - AnonymizeOrModifyInstance(modification, call); + AnonymizeOrModifyInstance(modification, call, false /* no transcoding */, + DicomTransferSyntax_LittleEndianImplicit /* unused */); + } + + + static void SetKeepSource(CleaningInstancesJob& job, + const Json::Value& body) + { + static const char* KEEP_SOURCE = "KeepSource"; + if (body.isMember(KEEP_SOURCE)) + { + job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE)); + } } @@ -178,11 +238,19 @@ ServerContext& context = OrthancRestApi::GetContext(call); std::unique_ptr job(new ResourceModificationJob(context)); - + job->SetModification(modification.release(), level, isAnonymization); job->SetOrigin(call); + SetKeepSource(*job, body); + + static const char* TRANSCODE = "Transcode"; + if (body.isMember(TRANSCODE)) + { + job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE)); + } context.AddChildInstances(*job, call.GetUriComponent("id", "")); + job->AddTrailingStep(); OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, body); @@ -227,7 +295,7 @@ toStore.SetParsedDicomFile(dicom); ServerContext& context = OrthancRestApi::GetContext(call); - StoreStatus status = context.Store(id, toStore); + StoreStatus status = context.Store(id, toStore, StoreInstanceMode_Default); if (status == StoreStatus_Failure) { @@ -236,7 +304,7 @@ if (sendAnswer) { - OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status); + OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, id); } } @@ -678,14 +746,10 @@ { job->AddSourceSeries(series[i]); } - + job->AddTrailingStep(); - static const char* KEEP_SOURCE = "KeepSource"; - if (request.isMember(KEEP_SOURCE)) - { - job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE)); - } + SetKeepSource(*job, request); static const char* REMOVE = "Remove"; if (request.isMember(REMOVE)) @@ -764,11 +828,7 @@ job->AddTrailingStep(); - static const char* KEEP_SOURCE = "KeepSource"; - if (request.isMember(KEEP_SOURCE)) - { - job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE)); - } + SetKeepSource(*job, request); OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, request); diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancRestApi/OrthancRestApi.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp Wed May 20 16:42:44 2020 +0200 @@ -63,11 +63,11 @@ void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call, DicomInstanceToStore& instance, - StoreStatus status) const + StoreStatus status, + const std::string& instanceId) const { Json::Value result; - SetupResourceAnswer(result, instance.GetHasher().HashInstance(), - ResourceType_Instance, status); + SetupResourceAnswer(result, instanceId, ResourceType_Instance, status); result["ParentPatient"] = instance.GetHasher().HashPatient(); result["ParentStudy"] = instance.GetHasher().HashStudy(); @@ -140,9 +140,9 @@ } std::string publicId; - StoreStatus status = context.Store(publicId, toStore); + StoreStatus status = context.Store(publicId, toStore, StoreInstanceMode_Default); - OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status); + OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, publicId); } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancRestApi/OrthancRestApi.h --- a/OrthancServer/OrthancRestApi/OrthancRestApi.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestApi.h Wed May 20 16:42:44 2020 +0200 @@ -107,9 +107,12 @@ static ServerIndex& GetIndex(RestApiCall& call); + // WARNING: "instanceId" can be different from + // "instance.GetHasher().HashInstance()" if transcoding is enabled void AnswerStoredInstance(RestApiPostCall& call, DicomInstanceToStore& instance, - StoreStatus status) const; + StoreStatus status, + const std::string& instanceId) const; void AnswerStoredResource(RestApiPostCall& call, const std::string& publicId, diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancRestApi/OrthancRestArchive.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed May 20 16:42:44 2020 +0200 @@ -46,6 +46,7 @@ { static const char* const KEY_RESOURCES = "Resources"; static const char* const KEY_EXTENDED = "Extended"; + static const char* const KEY_TRANSCODE = "Transcode"; static void AddResourcesOfInterestFromArray(ArchiveJob& job, const Json::Value& resources) @@ -98,11 +99,28 @@ } - static void GetJobParameters(bool& synchronous, /* out */ - bool& extended, /* out */ - int& priority, /* out */ - const Json::Value& body, /* in */ - const bool defaultExtended /* in */) + static DicomTransferSyntax GetTransferSyntax(const std::string& value) + { + DicomTransferSyntax syntax; + if (LookupTransferSyntax(syntax, value)) + { + return syntax; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unknown transfer syntax: " + value); + } + } + + + static void GetJobParameters(bool& synchronous, /* out */ + bool& extended, /* out */ + bool& transcode, /* out */ + DicomTransferSyntax& syntax, /* out */ + int& priority, /* out */ + const Json::Value& body, /* in */ + const bool defaultExtended /* in */) { synchronous = OrthancRestApi::IsSynchronousJobRequest (true /* synchronous by default */, body); @@ -118,6 +136,17 @@ { extended = defaultExtended; } + + if (body.type() == Json::objectValue && + body.isMember(KEY_TRANSCODE)) + { + transcode = true; + syntax = GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE)); + } + else + { + transcode = false; + } } @@ -175,12 +204,20 @@ Json::Value body; if (call.ParseJsonRequest(body)) { - bool synchronous, extended; + bool synchronous, extended, transcode; + DicomTransferSyntax transferSyntax; int priority; - GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED); + GetJobParameters(synchronous, extended, transcode, transferSyntax, + priority, body, DEFAULT_IS_EXTENDED); std::unique_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); AddResourcesOfInterest(*job, body); + + if (transcode) + { + job->SetTranscode(transferSyntax); + } + SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip"); } else @@ -208,10 +245,16 @@ { extended = false; } - + std::unique_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); job->AddResource(id); + static const char* const TRANSCODE = "transcode"; + if (call.HasArgument(TRANSCODE)) + { + job->SetTranscode(GetTransferSyntax(call.GetArgument(TRANSCODE, ""))); + } + SubmitJob(call.GetOutput(), context, job, 0 /* priority */, true /* synchronous */, id + ".zip"); } @@ -228,12 +271,20 @@ Json::Value body; if (call.ParseJsonRequest(body)) { - bool synchronous, extended; + bool synchronous, extended, transcode; + DicomTransferSyntax transferSyntax; int priority; - GetJobParameters(synchronous, extended, priority, body, DEFAULT_IS_EXTENDED); + GetJobParameters(synchronous, extended, transcode, transferSyntax, + priority, body, DEFAULT_IS_EXTENDED); std::unique_ptr job(new ArchiveJob(context, IS_MEDIA, extended)); job->AddResource(id); + + if (transcode) + { + job->SetTranscode(transferSyntax); + } + SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip"); } else diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancRestApi/OrthancRestModalities.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed May 20 16:42:44 2020 +0200 @@ -54,12 +54,14 @@ namespace Orthanc { static const char* const KEY_LEVEL = "Level"; - static const char* const KEY_QUERY = "Query"; + static const char* const KEY_LOCAL_AET = "LocalAet"; static const char* const KEY_NORMALIZE = "Normalize"; + static const char* const KEY_QUERY = "Query"; static const char* const KEY_RESOURCES = "Resources"; + static const char* const KEY_TARGET_AET = "TargetAet"; + static const char* const KEY_TIMEOUT = "Timeout"; static const char* const SOP_CLASS_UID = "SOPClassUID"; static const char* const SOP_INSTANCE_UID = "SOPInstanceUID"; - static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name) { @@ -68,35 +70,62 @@ } + static void InjectAssociationTimeout(DicomAssociationParameters& params, + const Json::Value& body) + { + if (body.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object"); + } + else if (body.isMember(KEY_TIMEOUT)) + { + // New in Orthanc 1.7.0 + params.SetTimeout(SerializationToolbox::ReadUnsignedInteger(body, KEY_TIMEOUT)); + } + } + + static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call, + const Json::Value& body) + { + const std::string& localAet = + OrthancRestApi::GetContext(call).GetDefaultLocalApplicationEntityTitle(); + const RemoteModalityParameters remote = + MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + DicomAssociationParameters params(localAet, remote); + InjectAssociationTimeout(params, body); + + return params; + } + + + static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call) + { + Json::Value body; + call.ParseJsonRequest(body); + return GetAssociationParameters(call, body); + } + + /*************************************************************************** * DICOM C-Echo SCU ***************************************************************************/ static void DicomEcho(RestApiPostCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); + DicomControlUserConnection connection(GetAssociationParameters(call)); - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - - try + if (connection.Echo()) { - DicomControlUserConnection connection(localAet, remote); - - if (connection.Echo()) - { - // Echo has succeeded - call.GetOutput().AnswerBuffer("{}", MimeType_Json); - return; - } + // Echo has succeeded + call.GetOutput().AnswerBuffer("{}", MimeType_Json); + return; } - catch (OrthancException&) + else { + // Echo has failed + call.GetOutput().SignalError(HttpStatus_500_InternalServerError); } - - // Echo has failed - call.GetOutput().SignalError(HttpStatus_500_InternalServerError); } @@ -188,7 +217,6 @@ static void DicomFindPatient(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindPatientTemplate(fields); @@ -197,14 +225,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindPatient(answers, connection, fields); } @@ -216,7 +240,6 @@ static void DicomFindStudy(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindStudyTemplate(fields); @@ -231,14 +254,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindStudy(answers, connection, fields); } @@ -250,7 +269,6 @@ static void DicomFindSeries(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindSeriesTemplate(fields); @@ -266,14 +284,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindSeries(answers, connection, fields); } @@ -285,7 +299,6 @@ static void DicomFindInstance(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap fields; DicomMap::SetupFindInstanceTemplate(fields); @@ -302,14 +315,10 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomFindAnswers answers(false); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); FindInstance(answers, connection, fields); } @@ -334,7 +343,6 @@ static void DicomFind(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); - ServerContext& context = OrthancRestApi::GetContext(call); DicomMap m; DicomMap::SetupFindPatientTemplate(m); @@ -343,11 +351,7 @@ return; } - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call)); DicomFindAnswers patients(false); FindPatient(patients, connection, m); @@ -605,16 +609,25 @@ ServerContext& context = OrthancRestApi::GetContext(call); std::string targetAet; + int timeout = -1; Json::Value body; if (call.ParseJsonRequest(body)) { - targetAet = SerializationToolbox::ReadString(body, "TargetAet"); + targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); + timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1); } else { body = Json::objectValue; - call.BodyToString(targetAet); + if (call.GetBodySize() > 0) + { + call.BodyToString(targetAet); + } + else + { + targetAet = context.GetDefaultLocalApplicationEntityTitle(); + } } std::unique_ptr job(new DicomMoveScuJob(context)); @@ -625,6 +638,12 @@ job->SetLocalAet(query.GetHandler().GetLocalAet()); job->SetRemoteModality(query.GetHandler().GetRemoteModality()); + if (timeout >= 0) + { + // New in Orthanc 1.7.0 + job->SetTimeout(static_cast(timeout)); + } + LOG(WARNING) << "Driving C-Move SCU on remote modality " << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle() << " to target modality " << targetAet; @@ -948,7 +967,7 @@ GetInstancesToExport(request, *job, remote, call); std::string localAet = Toolbox::GetJsonStringField - (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); + (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle()); std::string moveOriginatorAET = Toolbox::GetJsonStringField (request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle()); int moveOriginatorID = Toolbox::GetJsonIntegerField @@ -968,6 +987,12 @@ job->EnableStorageCommitment(true); } + // New in Orthanc 1.7.0 + if (request.isMember(KEY_TIMEOUT)) + { + job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT)); + } + OrthancRestApi::GetApi(call).SubmitCommandsJob (call, job.release(), true /* synchronous by default */, request); } @@ -975,18 +1000,12 @@ static void DicomStoreStraight(RestApiPostCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); - - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - - DicomUserConnection connection(localAet, remote); - connection.Open(); + Json::Value body = Json::objectValue; // No body + DicomStoreUserConnection connection(GetAssociationParameters(call, body)); std::string sopClassUid, sopInstanceUid; - connection.Store(sopClassUid, sopInstanceUid, - call.GetBodyData(), call.GetBodySize()); + connection.Store(sopClassUid, sopInstanceUid, call.GetBodyData(), + call.GetBodySize(), false /* Not a C-MOVE */, "", 0); Json::Value answer = Json::objectValue; answer[SOP_CLASS_UID] = sopClassUid; @@ -1020,15 +1039,18 @@ ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); std::string localAet = Toolbox::GetJsonStringField - (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle()); + (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle()); std::string targetAet = Toolbox::GetJsonStringField - (request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle()); + (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle()); const RemoteModalityParameters source = MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - DicomControlUserConnection connection(localAet, source); - + DicomAssociationParameters params(localAet, source); + InjectAssociationTimeout(params, request); + + DicomControlUserConnection connection(params); + for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++) { DicomMap resource; @@ -1114,21 +1136,31 @@ std::unique_ptr job(new OrthancPeerStoreJob(context)); GetInstancesToExport(request, *job, remote, call); + + static const char* TRANSCODE = "Transcode"; + if (request.type() == Json::objectValue && + request.isMember(TRANSCODE)) + { + job->SetTranscode(SerializationToolbox::ReadString(request, TRANSCODE)); + } - OrthancConfiguration::ReaderLock lock; - - WebServiceParameters peer; - if (lock.GetConfiguration().LookupOrthancPeer(peer, remote)) { - job->SetPeer(peer); - OrthancRestApi::GetApi(call).SubmitCommandsJob - (call, job.release(), true /* synchronous by default */, request); + OrthancConfiguration::ReaderLock lock; + + WebServiceParameters peer; + if (lock.GetConfiguration().LookupOrthancPeer(peer, remote)) + { + job->SetPeer(peer); + } + else + { + throw OrthancException(ErrorCode_UnknownResource, + "No peer with symbolic name: " + remote); + } } - else - { - throw OrthancException(ErrorCode_UnknownResource, - "No peer with symbolic name: " + remote); - } + + OrthancRestApi::GetApi(call).SubmitCommandsJob + (call, job.release(), true /* synchronous by default */, request); } static void PeerSystem(RestApiGetCall& call) @@ -1294,15 +1326,9 @@ static void DicomFindWorklist(RestApiPostCall& call) { - ServerContext& context = OrthancRestApi::GetContext(call); - Json::Value json; if (call.ParseJsonRequest(json)) { - const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); - const RemoteModalityParameters remote = - MyGetModalityUsingSymbolicName(call.GetUriComponent("id", "")); - std::unique_ptr query (ParsedDicomFile::CreateFromJson(json, static_cast(0), "" /* no private creator */)); @@ -1310,7 +1336,7 @@ DicomFindAnswers answers(true); { - DicomControlUserConnection connection(localAet, remote); + DicomControlUserConnection connection(GetAssociationParameters(call, json)); connection.FindWorklist(answers, *query); } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed May 20 16:42:44 2020 +0200 @@ -43,7 +43,7 @@ #include "../../Core/Images/Image.h" #include "../../Core/Images/ImageProcessing.h" #include "../../Core/Logging.h" -#include "../DefaultDicomImageDecoder.h" +#include "../../Core/MultiThreading/Semaphore.h" #include "../OrthancConfiguration.h" #include "../Search/DatabaseLookup.h" #include "../ServerContext.h" @@ -56,6 +56,14 @@ #include +/** + * This semaphore is used to limit the number of concurrent HTTP + * requests on CPU-intensive routes of the REST API, in order to + * prevent exhaustion of resources (new in Orthanc 1.7.0). + **/ +static Orthanc::Semaphore throttlingSemaphore_(4); // TODO => PARAMETER? + + namespace Orthanc { static void AnswerDicomAsJson(RestApiCall& call, @@ -547,44 +555,23 @@ { std::string publicId = call.GetUriComponent("id", ""); -#if ORTHANC_ENABLE_PLUGINS == 1 - if (context.GetPlugins().HasCustomImageDecoder()) - { - // TODO create a cache of file - std::string dicomContent; - context.ReadDicom(dicomContent, publicId); - decoded.reset(context.GetPlugins().DecodeUnsafe(dicomContent.c_str(), dicomContent.size(), frame)); - - /** - * Note that we call "DecodeUnsafe()": We do not fallback to - * the builtin decoder if no installed decoder plugin is able - * to decode the image. This allows us to take advantage of - * the cache below. - **/ - - if (handler.RequiresDicomTags() && - decoded.get() != NULL) - { - // TODO Optimize this lookup for photometric interpretation: - // It should be implemented by the plugin to avoid parsing - // twice the DICOM file - ParsedDicomFile parsed(dicomContent); - parsed.ExtractDicomSummary(dicom); - } - } -#endif + decoded.reset(context.DecodeDicomFrame(publicId, frame)); if (decoded.get() == NULL) { - // Use Orthanc's built-in decoder, using the cache to speed-up - // things on multi-frame images - ServerContext::DicomCacheLocker locker(context, publicId); - decoded.reset(DicomImageDecoder::Decode(locker.GetDicom(), frame)); - - if (handler.RequiresDicomTags()) - { - locker.GetDicom().ExtractDicomSummary(dicom); - } + throw OrthancException(ErrorCode_NotImplemented, + "Cannot decode DICOM instance with ID: " + publicId); + } + + if (handler.RequiresDicomTags()) + { + /** + * Retrieve a summary of the DICOM tags, which is + * necessary to deal with MONOCHROME1 photometric + * interpretation, and with windowing parameters. + **/ + ServerContext::DicomCacheLocker locker(context, publicId); + locker.GetDicom().ExtractDicomSummary(dicom); } } catch (OrthancException& e) @@ -938,6 +925,8 @@ template static void GetImage(RestApiGetCall& call) { + Semaphore::Locker locker(throttlingSemaphore_); + GetImageHandler handler(mode); IDecodedFrameHandler::Apply(call, handler); } @@ -945,6 +934,8 @@ static void GetRenderedFrame(RestApiGetCall& call) { + Semaphore::Locker locker(throttlingSemaphore_); + RenderedFrameHandler handler; IDecodedFrameHandler::Apply(call, handler); } @@ -952,6 +943,8 @@ static void GetMatlabImage(RestApiGetCall& call) { + Semaphore::Locker locker(throttlingSemaphore_); + ServerContext& context = OrthancRestApi::GetContext(call); std::string frameId = call.GetUriComponent("frame", "0"); @@ -967,21 +960,19 @@ } std::string publicId = call.GetUriComponent("id", ""); - std::string dicomContent; - context.ReadDicom(dicomContent, publicId); + std::unique_ptr decoded(context.DecodeDicomFrame(publicId, frame)); -#if ORTHANC_ENABLE_PLUGINS == 1 - IDicomImageDecoder& decoder = context.GetPlugins(); -#else - DefaultDicomImageDecoder decoder; // This is Orthanc's built-in decoder -#endif - - std::unique_ptr decoded(decoder.Decode(dicomContent.c_str(), dicomContent.size(), frame)); - - std::string result; - decoded->ToMatlabString(result); - - call.GetOutput().AnswerBuffer(result, MimeType_PlainText); + if (decoded.get() == NULL) + { + throw OrthancException(ErrorCode_NotImplemented, + "Cannot decode DICOM instance with ID: " + publicId); + } + else + { + std::string result; + decoded->ToMatlabString(result); + call.GetOutput().AnswerBuffer(result, MimeType_PlainText); + } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/QueryRetrieveHandler.cpp --- a/OrthancServer/QueryRetrieveHandler.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/QueryRetrieveHandler.cpp Wed May 20 16:42:44 2020 +0200 @@ -82,7 +82,8 @@ FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); { - DicomControlUserConnection connection(localAet_, modality_); + DicomAssociationParameters params(localAet_, modality_); + DicomControlUserConnection connection(params); connection.Find(answers_, level_, fixed, findNormalized_); } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerContext.cpp Wed May 20 16:42:44 2020 +0200 @@ -34,7 +34,9 @@ #include "PrecompiledHeadersServer.h" #include "ServerContext.h" +#include "../Core/DicomParsing/Internals/DicomImageDecoder.h" #include "../Core/Cache/SharedArchive.h" +#include "../Core/DicomParsing/DcmtkTranscoder.h" #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/FileStorage/StorageAccessor.h" #include "../Core/HttpServer/FilesystemHttpSender.h" @@ -82,7 +84,7 @@ { const ServerIndexChange& change = dynamic_cast(*obj.get()); - boost::recursive_mutex::scoped_lock lock(that->listenersMutex_); + boost::shared_lock lock(that->listenersMutex_); for (ServerListeners::iterator it = that->listeners_.begin(); it != that->listeners_.end(); ++it) { @@ -242,33 +244,74 @@ isJobsEngineUnserialized_(false), metricsRegistry_(new MetricsRegistry), isHttpServerSecure_(true), - isExecuteLuaEnabled_(false) + isExecuteLuaEnabled_(false), + overwriteInstances_(false), + dcmtkTranscoder_(new DcmtkTranscoder), + isIngestTranscoding_(false) { + try { - OrthancConfiguration::ReaderLock lock; + unsigned int lossyQuality; + + { + OrthancConfiguration::ReaderLock lock; - queryRetrieveArchive_.reset( - new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100))); - mediaArchive_.reset( - new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1))); - defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"); - jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); - saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true); - metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true)); + queryRetrieveArchive_.reset( + new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100))); + mediaArchive_.reset( + new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1))); + defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"); + jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); + saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true); + metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true)); + + // New configuration options in Orthanc 1.5.1 + findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always")); + limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); + limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); + + // New configuration option in Orthanc 1.6.0 + storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); + + // New options in Orthanc 1.7.0 + transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true); + builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After")); + lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90); - // New configuration options in Orthanc 1.5.1 - findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always")); - limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0); - limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0); + std::string s; + if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding")) + { + if (LookupTransferSyntax(ingestTransferSyntax_, s)) + { + isIngestTranscoding_ = true; + LOG(WARNING) << "Incoming DICOM instances will automatically be transcoded to " + << "transfer syntax: " << GetTransferSyntaxUid(ingestTransferSyntax_); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unknown transfer syntax for ingest transcoding: " + s); + } + } + else + { + isIngestTranscoding_ = false; + LOG(INFO) << "Automated transcoding of incoming DICOM instances is disabled"; + } + } - // New configuration option in Orthanc 1.6.0 - storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100))); - } + jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); - jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); - - listeners_.push_back(ServerListener(luaListener_, "Lua")); - changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); + listeners_.push_back(ServerListener(luaListener_, "Lua")); + changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100)); + + dynamic_cast(*dcmtkTranscoder_).SetLossyQuality(lossyQuality); + } + catch (OrthancException&) + { + Stop(); + throw; + } } @@ -288,7 +331,7 @@ if (!done_) { { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::unique_lock lock(listenersMutex_); listeners_.clear(); } @@ -338,9 +381,29 @@ } - StoreStatus ServerContext::Store(std::string& resultPublicId, - DicomInstanceToStore& dicom) + StoreStatus ServerContext::StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode) { + bool overwrite; + switch (mode) + { + case StoreInstanceMode_Default: + overwrite = overwriteInstances_; + break; + + case StoreInstanceMode_OverwriteDuplicate: + overwrite = true; + break; + + case StoreInstanceMode_IgnoreDuplicate: + overwrite = false; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + try { MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms"); @@ -355,7 +418,7 @@ bool accepted = true; { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::shared_lock lock(listenersMutex_); for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it) { @@ -404,7 +467,8 @@ typedef std::map InstanceMetadata; InstanceMetadata instanceMetadata; - StoreStatus status = index_.Store(instanceMetadata, dicom, attachments); + StoreStatus status = index_.Store( + instanceMetadata, dicom, attachments, overwrite); // Only keep the metadata for the "instance" level dicom.GetMetadata().clear(); @@ -444,7 +508,7 @@ if (status == StoreStatus_Success || status == StoreStatus_AlreadyStored) { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::shared_lock lock(listenersMutex_); for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it) { @@ -475,6 +539,59 @@ } + StoreStatus ServerContext::Store(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode) + { + if (!isIngestTranscoding_) + { + // No automated transcoding. This was the only path in Orthanc <= 1.6.1. + return StoreAfterTranscoding(resultPublicId, dicom, mode); + } + else + { + // Automated transcoding of incoming DICOM files + + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax( + sourceSyntax, dicom.GetParsedDicomFile().GetDcmtkObject()) || + sourceSyntax == ingestTransferSyntax_) + { + // No transcoding + return StoreAfterTranscoding(resultPublicId, dicom, mode); + } + else + { + std::set syntaxes; + syntaxes.insert(ingestTransferSyntax_); + + IDicomTranscoder::DicomImage source; + source.SetExternalBuffer(dicom.GetBufferData(), dicom.GetBufferSize()); + + IDicomTranscoder::DicomImage transcoded; + if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */)) + { + std::unique_ptr tmp(transcoded.ReleaseAsParsedDicomFile()); + + DicomInstanceToStore toStore; + toStore.SetParsedDicomFile(*tmp); + toStore.SetOrigin(dicom.GetOrigin()); + + StoreStatus ok = StoreAfterTranscoding(resultPublicId, toStore, mode); + assert(resultPublicId == tmp->GetHasher().HashInstance()); + + return ok; + } + else + { + // Cannot transcode => store the original file + return StoreAfterTranscoding(resultPublicId, dicom, mode); + } + } + } + } + + void ServerContext::AnswerAttachment(RestApiOutput& output, const std::string& resourceId, FileContentType content) @@ -739,7 +856,7 @@ #if ORTHANC_ENABLE_PLUGINS == 1 void ServerContext::SetPlugins(OrthancPlugins& plugins) { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::unique_lock lock(listenersMutex_); plugins_ = &plugins; @@ -752,7 +869,7 @@ void ServerContext::ResetPlugins() { - boost::recursive_mutex::scoped_lock lock(listenersMutex_); + boost::unique_lock lock(listenersMutex_); plugins_ = NULL; @@ -1086,4 +1203,159 @@ return NULL; } + + + ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId, + unsigned int frameIndex) + { + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) + { + // Use Orthanc's built-in decoder, using the cache to speed-up + // things on multi-frame images + ServerContext::DicomCacheLocker locker(*this, publicId); + std::unique_ptr decoded( + DicomImageDecoder::Decode(locker.GetDicom(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + } + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins() && + GetPlugins().HasCustomImageDecoder()) + { + // TODO: Store the raw buffer in the DicomCacheLocker + std::string dicomContent; + ReadDicom(dicomContent, publicId); + std::unique_ptr decoded( + GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + LOG(INFO) << "The installed image decoding plugins cannot handle an image, " + << "fallback to the built-in DCMTK decoder"; + } + } +#endif + + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + ServerContext::DicomCacheLocker locker(*this, publicId); + return DicomImageDecoder::Decode(locker.GetDicom(), frameIndex); + } + else + { + return NULL; // Built-in decoder is disabled + } + } + + + ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom, + unsigned int frameIndex) + { + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) + { + std::unique_ptr decoded( + DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + } + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins() && + GetPlugins().HasCustomImageDecoder()) + { + std::unique_ptr decoded( + GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex)); + if (decoded.get() != NULL) + { + return decoded.release(); + } + else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + LOG(INFO) << "The installed image decoding plugins cannot handle an image, " + << "fallback to the built-in DCMTK decoder"; + } + } +#endif + + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + return DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex); + } + else + { + return NULL; + } + } + + + void ServerContext::StoreWithTranscoding(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomStoreUserConnection& connection, + const std::string& dicom, + bool hasMoveOriginator, + const std::string& moveOriginatorAet, + uint16_t moveOriginatorId) + { + const void* data = dicom.empty() ? NULL : dicom.c_str(); + + if (!transcodeDicomProtocol_ || + !connection.GetParameters().GetRemoteModality().IsTranscodingAllowed()) + { + connection.Store(sopClassUid, sopInstanceUid, data, dicom.size(), + hasMoveOriginator, moveOriginatorAet, moveOriginatorId); + } + else + { + connection.Transcode(sopClassUid, sopInstanceUid, *this, data, dicom.size(), + hasMoveOriginator, moveOriginatorAet, moveOriginatorId); + } + } + + + bool ServerContext::Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before) + { + if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid)) + { + return true; + } + } + +#if ORTHANC_ENABLE_PLUGINS == 1 + if (HasPlugins() && + GetPlugins().HasCustomTranscoder()) + { + if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid)) + { + return true; + } + else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + LOG(INFO) << "The installed transcoding plugins cannot handle an image, " + << "fallback to the built-in DCMTK transcoder"; + } + } +#endif + + if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After) + { + return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid); + } + else + { + return false; + } + } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerContext.h Wed May 20 16:42:44 2020 +0200 @@ -40,6 +40,7 @@ #include "ServerJobs/IStorageCommitmentFactory.h" #include "../Core/Cache/MemoryCache.h" +#include "../Core/DicomParsing/IDicomTranscoder.h" namespace Orthanc @@ -64,6 +65,7 @@ **/ class ServerContext : public IStorageCommitmentFactory, + public IDicomTranscoder, private JobsRegistry::IObserver { public: @@ -98,7 +100,7 @@ } virtual void SignalStoredInstance(const std::string& publicId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) { context_.mainLua_.SignalStoredInstance(publicId, instance, simplifiedTags); @@ -201,7 +203,7 @@ #endif ServerListeners listeners_; - boost::recursive_mutex listenersMutex_; + boost::shared_mutex listenersMutex_; bool done_; bool haveJobsChanged_; @@ -221,9 +223,20 @@ std::unique_ptr metricsRegistry_; bool isHttpServerSecure_; bool isExecuteLuaEnabled_; + bool overwriteInstances_; std::unique_ptr storageCommitmentReports_; + bool transcodeDicomProtocol_; + std::unique_ptr dcmtkTranscoder_; + BuiltinDecoderTranscoderOrder builtinDecoderTranscoderOrder_; + bool isIngestTranscoding_; + DicomTransferSyntax ingestTransferSyntax_; + + StoreStatus StoreAfterTranscoding(std::string& resultPublicId, + DicomInstanceToStore& dicom, + StoreInstanceMode mode); + public: class DicomCacheLocker : public boost::noncopyable { @@ -275,7 +288,8 @@ size_t size); StoreStatus Store(std::string& resultPublicId, - DicomInstanceToStore& dicom); + DicomInstanceToStore& dicom, + StoreInstanceMode mode); void AnswerAttachment(RestApiOutput& output, const std::string& resourceId, @@ -426,6 +440,16 @@ return isExecuteLuaEnabled_; } + void SetOverwriteInstances(bool overwrite) + { + overwriteInstances_ = overwrite; + } + + bool IsOverwriteInstances() const + { + return overwriteInstances_; + } + virtual IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment(const std::string& jobId, const std::string& transactionUid, @@ -438,5 +462,26 @@ { return *storageCommitmentReports_; } + + ImageAccessor* DecodeDicomFrame(const std::string& publicId, + unsigned int frameIndex); + + ImageAccessor* DecodeDicomFrame(const DicomInstanceToStore& dicom, + unsigned int frameIndex); + + void StoreWithTranscoding(std::string& sopClassUid, + std::string& sopInstanceUid, + DicomStoreUserConnection& connection, + const std::string& dicom, + bool hasMoveOriginator, + const std::string& moveOriginatorAet, + uint16_t moveOriginatorId); + + // This method can be used even if the global option + // "TranscodeDicomProtocol" is set to "false" + virtual bool Transcode(DicomImage& target, + DicomImage& source /* in, "GetParsed()" possibly modified */, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerEnumerations.cpp --- a/OrthancServer/ServerEnumerations.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerEnumerations.cpp Wed May 20 16:42:44 2020 +0200 @@ -214,6 +214,29 @@ "should be \"Always\", \"Never\" or \"Answers\": " + value); } } + + + BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& value) + { + if (value == "Before") + { + return BuiltinDecoderTranscoderOrder_Before; + } + else if (value == "After") + { + return BuiltinDecoderTranscoderOrder_After; + } + else if (value == "Disabled") + { + return BuiltinDecoderTranscoderOrder_Disabled; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Configuration option \"BuiltinDecoderTranscoderOrder\" " + "should be \"After\", \"Before\" or \"Disabled\": " + value); + } + } std::string GetBasePath(ResourceType type, diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerEnumerations.h --- a/OrthancServer/ServerEnumerations.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerEnumerations.h Wed May 20 16:42:44 2020 +0200 @@ -90,6 +90,13 @@ FindStorageAccessMode_DiskOnLookupAndAnswer }; + enum StoreInstanceMode + { + StoreInstanceMode_Default, + StoreInstanceMode_OverwriteDuplicate, + StoreInstanceMode_IgnoreDuplicate + }; + /** * WARNING: Do not change the explicit values in the enumerations @@ -168,6 +175,13 @@ ChangeType_NewChildInstance = 4097 }; + enum BuiltinDecoderTranscoderOrder + { + BuiltinDecoderTranscoderOrder_Before, + BuiltinDecoderTranscoderOrder_After, + BuiltinDecoderTranscoderOrder_Disabled + }; + void InitializeServerEnumerations(); @@ -187,6 +201,8 @@ FindStorageAccessMode StringToFindStorageAccessMode(const std::string& str); + BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& str); + std::string EnumerationToString(FileContentType type); std::string GetFileContentMime(FileContentType type); diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerIndex.cpp Wed May 20 16:42:44 2020 +0200 @@ -675,7 +675,6 @@ db_(db), maximumStorageSize_(0), maximumPatients_(0), - overwrite_(false), mainDicomTagsRegistry_(new MainDicomTagsRegistry) { listener_.reset(new Listener(context)); @@ -753,7 +752,8 @@ StoreStatus ServerIndex::Store(std::map& instanceMetadata, DicomInstanceToStore& instanceToStore, - const Attachments& attachments) + const Attachments& attachments, + bool overwrite) { boost::mutex::scoped_lock lock(mutex_); @@ -784,7 +784,7 @@ { // The instance already exists - if (overwrite_) + if (overwrite) { // Overwrite the old instance LOG(INFO) << "Overwriting instance: " << hashInstance; @@ -1660,12 +1660,6 @@ StandaloneRecycling(); } - void ServerIndex::SetOverwriteInstances(bool overwrite) - { - boost::mutex::scoped_lock lock(mutex_); - overwrite_ = overwrite; - } - void ServerIndex::StandaloneRecycling() { diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerIndex.h Wed May 20 16:42:44 2020 +0200 @@ -71,7 +71,6 @@ uint64_t maximumStorageSize_; unsigned int maximumPatients_; - bool overwrite_; std::unique_ptr mainDicomTagsRegistry_; static void FlushThread(ServerIndex* that, @@ -139,11 +138,10 @@ // "count == 0" means no limit on the number of patients void SetMaximumPatientCount(unsigned int count); - void SetOverwriteInstances(bool overwrite); - StoreStatus Store(std::map& instanceMetadata, DicomInstanceToStore& instance, - const Attachments& attachments); + const Attachments& attachments, + bool overwrite); void GetGlobalStatistics(/* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/ServerJobs/ArchiveJob.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -37,6 +37,7 @@ #include "../../Core/Cache/SharedArchive.h" #include "../../Core/Compression/HierarchicalZipWriter.h" #include "../../Core/DicomParsing/DicomDirWriter.h" +#include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" #include "../OrthancConfiguration.h" @@ -55,6 +56,7 @@ static const char* const KEY_DESCRIPTION = "Description"; static const char* const KEY_INSTANCES_COUNT = "InstancesCount"; static const char* const KEY_UNCOMPRESSED_SIZE_MB = "UncompressedSizeMB"; +static const char* const KEY_TRANSCODE = "Transcode"; namespace Orthanc @@ -399,7 +401,9 @@ void Apply(HierarchicalZipWriter& writer, ServerContext& context, DicomDirWriter* dicomDir, - const std::string& dicomDirFolder) const + const std::string& dicomDirFolder, + bool transcode, + DicomTransferSyntax transferSyntax) const { switch (type_) { @@ -426,14 +430,54 @@ } //boost::this_thread::sleep(boost::posix_time::milliseconds(300)); - + writer.OpenFile(filename_.c_str()); - writer.Write(content); + + bool transcodeSuccess = false; + + std::unique_ptr parsed; + + if (transcode) + { + // New in Orthanc 1.7.0 + std::set syntaxes; + syntaxes.insert(transferSyntax); + + IDicomTranscoder::DicomImage source, transcoded; + source.SetExternalBuffer(content); + + if (context.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */)) + { + writer.Write(transcoded.GetBufferData(), transcoded.GetBufferSize()); - if (dicomDir != NULL) + if (dicomDir != NULL) + { + std::unique_ptr tmp(transcoded.ReleaseAsParsedDicomFile()); + dicomDir->Add(dicomDirFolder, filename_, *tmp); + } + + transcodeSuccess = true; + } + else + { + LOG(INFO) << "Cannot transcode instance " << instanceId_ + << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax); + } + } + + if (!transcodeSuccess) { - ParsedDicomFile parsed(content); - dicomDir->Add(dicomDirFolder, filename_, parsed); + writer.Write(content); + + if (dicomDir != NULL) + { + if (parsed.get() == NULL) + { + parsed.reset(new ParsedDicomFile(content)); + } + + dicomDir->Add(dicomDirFolder, filename_, *parsed); + } } break; @@ -454,14 +498,16 @@ ServerContext& context, size_t index, DicomDirWriter* dicomDir, - const std::string& dicomDirFolder) const + const std::string& dicomDirFolder, + bool transcode, + DicomTransferSyntax transferSyntax) const { if (index >= commands_.size()) { throw OrthancException(ErrorCode_ParameterOutOfRange); } - commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder); + commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder, transcode, transferSyntax); } public: @@ -496,20 +542,26 @@ return uncompressedSize_; } + // "media" flavor (with DICOMDIR) void Apply(HierarchicalZipWriter& writer, ServerContext& context, size_t index, DicomDirWriter& dicomDir, - const std::string& dicomDirFolder) const + const std::string& dicomDirFolder, + bool transcode, + DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder); + ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder, transcode, transferSyntax); } + // "archive" flavor (without DICOMDIR) void Apply(HierarchicalZipWriter& writer, ServerContext& context, - size_t index) const + size_t index, + bool transcode, + DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, context, index, NULL, ""); + ApplyInternal(writer, context, index, NULL, "", transcode, transferSyntax); } void AddOpenDirectory(const std::string& filename) @@ -740,7 +792,9 @@ return commands_.GetSize() + 1; } - void RunStep(size_t index) + void RunStep(size_t index, + bool transcode, + DicomTransferSyntax transferSyntax) { if (index > commands_.GetSize()) { @@ -764,12 +818,13 @@ if (isMedia_) { assert(dicomDir_.get() != NULL); - commands_.Apply(*zip_, context_, index, *dicomDir_, MEDIA_IMAGES_FOLDER); + commands_.Apply(*zip_, context_, index, *dicomDir_, + MEDIA_IMAGES_FOLDER, transcode, transferSyntax); } else { assert(dicomDir_.get() == NULL); - commands_.Apply(*zip_, context_, index); + commands_.Apply(*zip_, context_, index, transcode, transferSyntax); } } } @@ -795,7 +850,9 @@ enableExtendedSopClass_(enableExtendedSopClass), currentStep_(0), instancesCount_(0), - uncompressedSize_(0) + uncompressedSize_(0), + transcode_(false), + transferSyntax_(DicomTransferSyntax_LittleEndianImplicit) { } @@ -854,6 +911,20 @@ } } + + void ArchiveJob::SetTranscode(DicomTransferSyntax transferSyntax) + { + if (writer_.get() != NULL) // Already started + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = true; + transferSyntax_ = transferSyntax; + } + } + void ArchiveJob::Reset() { @@ -954,7 +1025,7 @@ } else { - writer_->RunStep(currentStep_); + writer_->RunStep(currentStep_, transcode_, transferSyntax_); currentStep_ ++; @@ -1006,6 +1077,11 @@ value[KEY_INSTANCES_COUNT] = instancesCount_; value[KEY_UNCOMPRESSED_SIZE_MB] = static_cast(uncompressedSize_ / MEGA_BYTES); + + if (transcode_) + { + value[KEY_TRANSCODE] = GetTransferSyntaxUid(transferSyntax_); + } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/ArchiveJob.h --- a/OrthancServer/ServerJobs/ArchiveJob.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/ArchiveJob.h Wed May 20 16:42:44 2020 +0200 @@ -69,6 +69,10 @@ uint64_t uncompressedSize_; std::string mediaArchiveId_; + // New in Orthanc 1.7.0 + bool transcode_; + DicomTransferSyntax transferSyntax_; + void FinalizeTarget(); public: @@ -89,6 +93,8 @@ void AddResource(const std::string& publicId); + void SetTranscode(DicomTransferSyntax transferSyntax); + virtual void Reset() ORTHANC_OVERRIDE; virtual void Start() ORTHANC_OVERRIDE; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/CleaningInstancesJob.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/CleaningInstancesJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,120 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "CleaningInstancesJob.h" + +#include "../../Core/SerializationToolbox.h" +#include "../ServerContext.h" + + +namespace Orthanc +{ + bool CleaningInstancesJob::HandleTrailingStep() + { + if (!keepSource_) + { + const size_t n = GetInstancesCount(); + + for (size_t i = 0; i < n; i++) + { + Json::Value tmp; + context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); + } + } + + return true; + } + + + void CleaningInstancesJob::SetKeepSource(bool keep) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + keepSource_ = keep; + } + + + static const char* KEEP_SOURCE = "KeepSource"; + + + CleaningInstancesJob::CleaningInstancesJob(ServerContext& context, + const Json::Value& serialized, + bool defaultKeepSource) : + SetOfInstancesJob(serialized), // (*) + context_(context) + { + if (!HasTrailingStep()) + { + // Should have been set by (*) + throw OrthancException(ErrorCode_InternalError); + } + + if (serialized.isMember(KEEP_SOURCE)) + { + keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE); + } + else + { + keepSource_ = defaultKeepSource; + } + } + + + bool CleaningInstancesJob::Serialize(Json::Value& target) + { + if (!SetOfInstancesJob::Serialize(target)) + { + return false; + } + else + { + target[KEEP_SOURCE] = keepSource_; + return true; + } + } + + + void CleaningInstancesJob::Start() + { + if (!HasTrailingStep()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "AddTrailingStep() should have been called before submitting the job"); + } + + SetOfInstancesJob::Start(); + } +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/CleaningInstancesJob.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/CleaningInstancesJob.h Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,79 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../../Core/JobsEngine/SetOfInstancesJob.h" + +namespace Orthanc +{ + class ServerContext; + + class CleaningInstancesJob : public SetOfInstancesJob + { + private: + ServerContext& context_; + bool keepSource_; + + protected: + virtual bool HandleTrailingStep(); + + public: + CleaningInstancesJob(ServerContext& context, + bool keepSource) : + context_(context), + keepSource_(keepSource) + { + } + + CleaningInstancesJob(ServerContext& context, + const Json::Value& serialized, + bool defaultKeepSource); + + ServerContext& GetContext() const + { + return context_; + } + + bool IsKeepSource() const + { + return keepSource_; + } + + void SetKeepSource(bool keep); + + virtual bool Serialize(Json::Value& target); + + virtual void Start(); + }; +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/DicomModalityStoreJob.cpp --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -35,6 +35,7 @@ #include "DicomModalityStoreJob.h" #include "../../Core/Compatibility.h" +#include "../../Core/DicomNetworking/DicomAssociation.h" #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" @@ -47,7 +48,7 @@ { if (connection_.get() == NULL) { - connection_.reset(new DicomUserConnection(localAet_, remote_)); + connection_.reset(new DicomStoreUserConnection(parameters_)); } } @@ -58,7 +59,7 @@ OpenConnection(); LOG(INFO) << "Sending instance " << instance << " to modality \"" - << remote_.GetApplicationEntityTitle() << "\""; + << parameters_.GetRemoteModality().GetApplicationEntityTitle() << "\""; std::string dicom; @@ -73,15 +74,8 @@ } std::string sopClassUid, sopInstanceUid; - - if (HasMoveOriginator()) - { - connection_->Store(sopClassUid, sopInstanceUid, dicom, moveOriginatorAet_, moveOriginatorId_); - } - else - { - connection_->Store(sopClassUid, sopInstanceUid, dicom); - } + context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom, + HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_); if (storageCommitment_) { @@ -96,7 +90,10 @@ if (sopClassUids_.size() == GetInstancesCount()) { - const std::string& remoteAet = remote_.GetApplicationEntityTitle(); + assert(IsStarted()); + connection_.reset(NULL); + + const std::string& remoteAet = parameters_.GetRemoteModality().GetApplicationEntityTitle(); LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet; @@ -105,12 +102,10 @@ context_.GetStorageCommitmentReports().Store( transactionUid_, new StorageCommitmentReports::Report(remoteAet)); - assert(IsStarted()); - OpenConnection(); - std::vector a(sopClassUids_.begin(), sopClassUids_.end()); std::vector b(sopInstanceUids_.begin(), sopInstanceUids_.end()); - connection_->RequestStorageCommitment(transactionUid_, a, b); + + DicomAssociation::RequestStorageCommitment(parameters_, transactionUid_, a, b); } } @@ -128,7 +123,6 @@ DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) : context_(context), - localAet_("ORTHANC"), moveOriginatorId_(0), // By default, not a C-MOVE storageCommitment_(false) // By default, no storage commitment { @@ -144,7 +138,7 @@ } else { - localAet_ = aet; + parameters_.SetLocalApplicationEntityTitle(aet); } } @@ -157,11 +151,24 @@ } else { - remote_ = remote; + parameters_.SetRemoteModality(remote); } } + void DicomModalityStoreJob::SetTimeout(uint32_t seconds) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetTimeout(seconds); + } + } + + const std::string& DicomModalityStoreJob::GetMoveOriginatorAet() const { if (HasMoveOriginator()) @@ -249,8 +256,8 @@ { SetOfInstancesJob::GetPublicContent(value); - value["LocalAet"] = localAet_; - value["RemoteAet"] = remote_.GetApplicationEntityTitle(); + value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle(); + value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle(); if (HasMoveOriginator()) { @@ -265,8 +272,6 @@ } - static const char* LOCAL_AET = "LocalAet"; - static const char* REMOTE = "Remote"; static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet"; static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId"; static const char* STORAGE_COMMITMENT = "StorageCommitment"; @@ -277,12 +282,12 @@ SetOfInstancesJob(serialized), context_(context) { - localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); - remote_ = RemoteModalityParameters(serialized[REMOTE]); moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET); moveOriginatorId_ = static_cast (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID)); EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT)); + + parameters_ = DicomAssociationParameters::UnserializeJob(serialized); } @@ -294,8 +299,7 @@ } else { - target[LOCAL_AET] = localAet_; - remote_.Serialize(target[REMOTE], true /* force advanced format */); + parameters_.SerializeJob(target); target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_; target[MOVE_ORIGINATOR_ID] = moveOriginatorId_; target[STORAGE_COMMITMENT] = storageCommitment_; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/DicomModalityStoreJob.h --- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h Wed May 20 16:42:44 2020 +0200 @@ -35,7 +35,9 @@ #include "../../Core/Compatibility.h" #include "../../Core/JobsEngine/SetOfInstancesJob.h" -#include "../../Core/DicomNetworking/DicomUserConnection.h" +#include "../../Core/DicomNetworking/DicomStoreUserConnection.h" + +#include namespace Orthanc { @@ -44,13 +46,12 @@ class DicomModalityStoreJob : public SetOfInstancesJob { private: - ServerContext& context_; - std::string localAet_; - RemoteModalityParameters remote_; - std::string moveOriginatorAet_; - uint16_t moveOriginatorId_; - std::unique_ptr connection_; - bool storageCommitment_; + ServerContext& context_; + DicomAssociationParameters parameters_; + std::string moveOriginatorAet_; + uint16_t moveOriginatorId_; + std::unique_ptr connection_; + bool storageCommitment_; // For storage commitment std::string transactionUid_; @@ -72,19 +73,16 @@ DicomModalityStoreJob(ServerContext& context, const Json::Value& serialized); - const std::string& GetLocalAet() const + const DicomAssociationParameters& GetParameters() const { - return localAet_; + return parameters_; } void SetLocalAet(const std::string& aet); - const RemoteModalityParameters& GetRemoteModality() const - { - return remote_; - } + void SetRemoteModality(const RemoteModalityParameters& remote); - void SetRemoteModality(const RemoteModalityParameters& remote); + void SetTimeout(uint32_t seconds); bool HasMoveOriginator() const { @@ -112,5 +110,10 @@ virtual void Reset() ORTHANC_OVERRIDE; void EnableStorageCommitment(bool enabled); + + bool HasStorageCommitment() const + { + return storageCommitment_; + } }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/DicomMoveScuJob.cpp --- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -40,6 +40,7 @@ static const char* const TARGET_AET = "TargetAet"; static const char* const REMOTE = "Remote"; static const char* const QUERY = "Query"; +static const char* const TIMEOUT = "Timeout"; namespace Orthanc { @@ -96,7 +97,7 @@ { if (connection_.get() == NULL) { - connection_.reset(new DicomControlUserConnection(localAet_, remote_)); + connection_.reset(new DicomControlUserConnection(parameters_)); } connection_->Move(targetAet_, findAnswer); @@ -152,7 +153,7 @@ } else { - localAet_ = aet; + parameters_.SetLocalApplicationEntityTitle(aet); } } @@ -178,7 +179,20 @@ } else { - remote_ = remote; + parameters_.SetRemoteModality(remote); + } + } + + + void DicomMoveScuJob::SetTimeout(uint32_t seconds) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parameters_.SetTimeout(seconds); } } @@ -192,9 +206,9 @@ void DicomMoveScuJob::GetPublicContent(Json::Value& value) { SetOfCommandsJob::GetPublicContent(value); - - value["LocalAet"] = localAet_; - value["RemoteAet"] = remote_.GetApplicationEntityTitle(); + + value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle(); + value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle(); value["Query"] = query_; } @@ -205,9 +219,8 @@ context_(context), query_(Json::arrayValue) { - localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); + parameters_ = DicomAssociationParameters::UnserializeJob(serialized); targetAet_ = SerializationToolbox::ReadString(serialized, TARGET_AET); - remote_ = RemoteModalityParameters(serialized[REMOTE]); if (serialized.isMember(QUERY) && serialized[QUERY].type() == Json::arrayValue) @@ -225,10 +238,9 @@ } else { - target[LOCAL_AET] = localAet_; + parameters_.SerializeJob(target); target[TARGET_AET] = targetAet_; target[QUERY] = query_; - remote_.Serialize(target[REMOTE], true /* force advanced format */); return true; } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/DicomMoveScuJob.h --- a/OrthancServer/ServerJobs/DicomMoveScuJob.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/DicomMoveScuJob.h Wed May 20 16:42:44 2020 +0200 @@ -49,11 +49,10 @@ class Command; class Unserializer; - ServerContext& context_; - std::string localAet_; - std::string targetAet_; - RemoteModalityParameters remote_; - Json::Value query_; + ServerContext& context_; + DicomAssociationParameters parameters_; + std::string targetAet_; + Json::Value query_; std::unique_ptr connection_; @@ -73,28 +72,25 @@ void AddFindAnswer(QueryRetrieveHandler& query, size_t i); - - const std::string& GetLocalAet() const + + const DicomAssociationParameters& GetParameters() const { - return localAet_; + return parameters_; } + + void SetLocalAet(const std::string& aet); - void SetLocalAet(const std::string& aet); + void SetRemoteModality(const RemoteModalityParameters& remote); + + void SetTimeout(uint32_t timeout); const std::string& GetTargetAet() const { return targetAet_; } - + void SetTargetAet(const std::string& aet); - const RemoteModalityParameters& GetRemoteModality() const - { - return remote_; - } - - void SetRemoteModality(const RemoteModalityParameters& remote); - virtual void Stop(JobStopReason reason); virtual void GetJobType(std::string& target) diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/LuaJobManager.cpp --- a/OrthancServer/ServerJobs/LuaJobManager.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/LuaJobManager.cpp Wed May 20 16:42:44 2020 +0200 @@ -200,11 +200,13 @@ } - size_t LuaJobManager::Lock::AddStoreScuOperation(const std::string& localAet, + size_t LuaJobManager::Lock::AddStoreScuOperation(ServerContext& context, + const std::string& localAet, const RemoteModalityParameters& modality) { assert(jobLock_.get() != NULL); - return jobLock_->AddOperation(new StoreScuOperation(that_.connectionManager_, localAet, modality)); + return jobLock_->AddOperation(new StoreScuOperation( + context, that_.connectionManager_, localAet, modality)); } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/LuaJobManager.h --- a/OrthancServer/ServerJobs/LuaJobManager.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/LuaJobManager.h Wed May 20 16:42:44 2020 +0200 @@ -91,7 +91,8 @@ size_t AddDeleteResourceOperation(ServerContext& context); - size_t AddStoreScuOperation(const std::string& localAet, + size_t AddStoreScuOperation(ServerContext& context, + const std::string& localAet, const RemoteModalityParameters& modality); size_t AddStorePeerOperation(const WebServiceParameters& peer); diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/MergeStudyJob.cpp --- a/OrthancServer/ServerJobs/MergeStudyJob.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -48,7 +48,7 @@ // Add all the instances of the series as to be processed std::list instances; - context_.GetIndex().GetChildren(instances, series); + GetContext().GetIndex().GetChildren(instances, series); for (std::list::const_iterator it = instances.begin(); it != instances.end(); ++it) @@ -68,7 +68,7 @@ else { std::list series; - context_.GetIndex().GetChildren(series, study); + GetContext().GetIndex().GetChildren(series, study); for (std::list::const_iterator it = series.begin(); it != series.end(); ++it) @@ -81,6 +81,12 @@ bool MergeStudyJob::HandleInstance(const std::string& instance) { + if (!HasTrailingStep()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "AddTrailingStep() should have been called after AddSourceXXX()"); + } + /** * Retrieve the DICOM instance to be modified **/ @@ -89,7 +95,7 @@ try { - ServerContext::DicomCacheLocker locker(context_, instance); + ServerContext::DicomCacheLocker locker(GetContext(), instance); modified.reset(locker.GetDicom().Clone(true)); } catch (OrthancException&) @@ -145,7 +151,8 @@ toStore.SetParsedDicomFile(*modified); std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + if (GetContext().Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { LOG(ERROR) << "Error while storing a modified instance " << instance; return false; @@ -155,27 +162,9 @@ } - bool MergeStudyJob::HandleTrailingStep() - { - if (!keepSource_) - { - const size_t n = GetInstancesCount(); - - for (size_t i = 0; i < n; i++) - { - Json::Value tmp; - context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); - } - } - - return true; - } - - MergeStudyJob::MergeStudyJob(ServerContext& context, const std::string& targetStudy) : - context_(context), - keepSource_(false), + CleaningInstancesJob(context, false /* by default, remove source instances */), targetStudy_(targetStudy) { /** @@ -184,7 +173,7 @@ ResourceType type; - if (!context_.GetIndex().LookupResourceType(type, targetStudy) || + if (!GetContext().GetIndex().LookupResourceType(type, targetStudy) || type != ResourceType_Study) { throw OrthancException(ErrorCode_UnknownResource, @@ -201,7 +190,7 @@ DicomTag::AddTagsForModule(removals_, DicomModule_Study); std::list instances; - context_.GetIndex().GetChildInstances(instances, targetStudy); + GetContext().GetIndex().GetChildInstances(instances, targetStudy); if (instances.empty()) { @@ -211,7 +200,7 @@ DicomMap dicom; { - ServerContext::DicomCacheLocker locker(context_, instances.front()); + ServerContext::DicomCacheLocker locker(GetContext(), instances.front()); locker.GetDicom().ExtractDicomSummary(dicom); } @@ -259,7 +248,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries)) + else if (!GetContext().GetIndex().LookupResourceType(level, studyOrSeries)) { throw OrthancException(ErrorCode_UnknownResource, "Cannot find this resource: " + studyOrSeries); @@ -294,7 +283,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study)) + else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study)) { throw OrthancException(ErrorCode_UnknownResource, "This resource is not a series: " + series); @@ -320,7 +309,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupResourceType(actualLevel, study) || + else if (!GetContext().GetIndex().LookupResourceType(actualLevel, study) || actualLevel != ResourceType_Study) { throw OrthancException(ErrorCode_UnknownResource, @@ -333,25 +322,13 @@ } - void MergeStudyJob::SetKeepSource(bool keep) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - keepSource_ = keep; - } - - void MergeStudyJob::GetPublicContent(Json::Value& value) { - SetOfInstancesJob::GetPublicContent(value); + CleaningInstancesJob::GetPublicContent(value); value["TargetStudy"] = targetStudy_; } - static const char* KEEP_SOURCE = "KeepSource"; static const char* TARGET_STUDY = "TargetStudy"; static const char* REPLACEMENTS = "Replacements"; static const char* REMOVALS = "Removals"; @@ -361,8 +338,8 @@ MergeStudyJob::MergeStudyJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), // (*) - context_(context) + CleaningInstancesJob(context, serialized, + false /* by default, remove source instances */) // (*) { if (!HasTrailingStep()) { @@ -370,7 +347,6 @@ throw OrthancException(ErrorCode_InternalError); } - keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE); targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY); SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS); SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS); @@ -381,13 +357,12 @@ bool MergeStudyJob::Serialize(Json::Value& target) { - if (!SetOfInstancesJob::Serialize(target)) + if (!CleaningInstancesJob::Serialize(target)) { return false; } else { - target[KEEP_SOURCE] = keepSource_; target[TARGET_STUDY] = targetStudy_; SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS); SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS); diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/MergeStudyJob.h --- a/OrthancServer/ServerJobs/MergeStudyJob.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/MergeStudyJob.h Wed May 20 16:42:44 2020 +0200 @@ -34,22 +34,20 @@ #pragma once #include "../../Core/DicomFormat/DicomMap.h" -#include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../DicomInstanceOrigin.h" +#include "CleaningInstancesJob.h" namespace Orthanc { class ServerContext; - class MergeStudyJob : public SetOfInstancesJob + class MergeStudyJob : public CleaningInstancesJob { private: typedef std::map SeriesUidMap; typedef std::map Replacements; - ServerContext& context_; - bool keepSource_; std::string targetStudy_; Replacements replacements_; std::set removals_; @@ -61,12 +59,9 @@ void AddSourceStudyInternal(const std::string& study); - protected: virtual bool HandleInstance(const std::string& instance); - virtual bool HandleTrailingStep(); - public: MergeStudyJob(ServerContext& context, const std::string& targetStudy); @@ -85,13 +80,6 @@ void AddSourceSeries(const std::string& series); - bool IsKeepSource() const - { - return keepSource_; - } - - void SetKeepSource(bool keep); - void SetOrigin(const DicomInstanceOrigin& origin); void SetOrigin(const RestApiCall& call); diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp --- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp Wed May 20 16:42:44 2020 +0200 @@ -113,7 +113,7 @@ toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, instance.GetId()); std::string modifiedId; - context_.Store(modifiedId, toStore); + context_.Store(modifiedId, toStore, StoreInstanceMode_Default); // Only chain with other commands if this command succeeds outputs.Append(new DicomInstanceOperationValue(instance.GetServerContext(), modifiedId)); diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp --- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp Wed May 20 16:42:44 2020 +0200 @@ -35,6 +35,7 @@ #include "StoreScuOperation.h" #include "DicomInstanceOperationValue.h" +#include "../../ServerContext.h" #include "../../../Core/Logging.h" #include "../../../Core/OrthancException.h" @@ -64,7 +65,8 @@ instance.ReadDicom(dicom); std::string sopClassUid, sopInstanceUid; // Unused - lock.GetConnection().Store(sopClassUid, sopInstanceUid, dicom); + context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom, + false /* Not a C-MOVE */, "", 0); } catch (OrthancException& e) { @@ -85,8 +87,10 @@ } - StoreScuOperation::StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + StoreScuOperation::StoreScuOperation(ServerContext& context, + TimeoutDicomConnectionManager& connectionManager, const Json::Value& serialized) : + context_(context), connectionManager_(connectionManager) { if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" || diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/Operations/StoreScuOperation.h --- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.h Wed May 20 16:42:44 2020 +0200 @@ -38,24 +38,30 @@ namespace Orthanc { + class ServerContext; + class StoreScuOperation : public IJobOperation { private: + ServerContext& context_; TimeoutDicomConnectionManager& connectionManager_; std::string localAet_; RemoteModalityParameters modality_; public: - StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + StoreScuOperation(ServerContext& context, + TimeoutDicomConnectionManager& connectionManager, const std::string& localAet, const RemoteModalityParameters& modality) : + context_(context), connectionManager_(connectionManager), localAet_(localAet), modality_(modality) { } - StoreScuOperation(TimeoutDicomConnectionManager& connectionManager, + StoreScuOperation(ServerContext& context, + TimeoutDicomConnectionManager& connectionManager, const Json::Value& serialized); const std::string& GetLocalAet() const diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/OrthancJobUnserializer.cpp --- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Wed May 20 16:42:44 2020 +0200 @@ -127,7 +127,7 @@ else if (type == "StoreScu") { return new StoreScuOperation( - context_.GetLuaScripting().GetDicomConnectionManager(), source); + context_, context_.GetLuaScripting().GetDicomConnectionManager(), source); } else if (type == "SystemCall") { diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp --- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -35,8 +35,11 @@ #include "OrthancPeerStoreJob.h" #include "../../Core/Logging.h" +#include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" +#include + namespace Orthanc { @@ -55,7 +58,31 @@ try { - context_.ReadDicom(client_->GetBody(), instance); + if (transcode_) + { + std::string dicom; + context_.ReadDicom(dicom, instance); + + std::set syntaxes; + syntaxes.insert(transferSyntax_); + + IDicomTranscoder::DicomImage source, transcoded; + source.SetExternalBuffer(dicom); + + if (context_.Transcode(transcoded, source, syntaxes, true)) + { + client_->GetBody().assign(reinterpret_cast(transcoded.GetBufferData()), + transcoded.GetBufferSize()); + } + else + { + client_->GetBody().swap(dicom); + } + } + else + { + context_.ReadDicom(client_->GetBody(), instance); + } } catch (OrthancException& e) { @@ -94,6 +121,61 @@ } + DicomTransferSyntax OrthancPeerStoreJob::GetTransferSyntax() const + { + if (transcode_) + { + return transferSyntax_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancPeerStoreJob::SetTranscode(DicomTransferSyntax syntax) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = true; + transferSyntax_ = syntax; + } + } + + + void OrthancPeerStoreJob::SetTranscode(const std::string& transferSyntaxUid) + { + DicomTransferSyntax s; + if (LookupTransferSyntax(s, transferSyntaxUid)) + { + SetTranscode(s); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Unknown transfer syntax UID: " + transferSyntaxUid); + } + } + + + void OrthancPeerStoreJob::ClearTranscode() + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = false; + } + } + + void OrthancPeerStoreJob::Stop(JobStopReason reason) // For pausing jobs { client_.reset(NULL); @@ -109,17 +191,33 @@ false /* allow simple format if possible */, false /* don't include passwords */); value["Peer"] = v; + + if (transcode_) + { + value["Transcode"] = GetTransferSyntaxUid(transferSyntax_); + } } static const char* PEER = "Peer"; + static const char* TRANSCODE = "Transcode"; OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context, const Json::Value& serialized) : SetOfInstancesJob(serialized), context_(context) { + assert(serialized.type() == Json::objectValue); peer_ = WebServiceParameters(serialized[PEER]); + + if (serialized.isMember(TRANSCODE)) + { + SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE)); + } + else + { + transcode_ = false; + } } @@ -131,9 +229,16 @@ } else { + assert(target.type() == Json::objectValue); peer_.Serialize(target[PEER], true /* force advanced format */, true /* include passwords */); + + if (transcode_) + { + target[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_); + } + return true; } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/OrthancPeerStoreJob.h --- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h Wed May 20 16:42:44 2020 +0200 @@ -48,6 +48,8 @@ ServerContext& context_; WebServiceParameters peer_; std::unique_ptr client_; + bool transcode_; + DicomTransferSyntax transferSyntax_; protected: virtual bool HandleInstance(const std::string& instance); @@ -56,7 +58,8 @@ public: OrthancPeerStoreJob(ServerContext& context) : - context_(context) + context_(context), + transcode_(false) { } @@ -70,6 +73,19 @@ return peer_; } + bool IsTranscode() const + { + return transcode_; + } + + DicomTransferSyntax GetTransferSyntax() const; + + void SetTranscode(DicomTransferSyntax syntax); + + void SetTranscode(const std::string& transferSyntaxUid); + + void ClearTranscode(); + virtual void Stop(JobStopReason reason); // For pausing jobs virtual void GetJobType(std::string& target) diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/ResourceModificationJob.cpp --- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -38,6 +38,10 @@ #include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" +#include +#include +#include + namespace Orthanc { class ResourceModificationJob::Output : public boost::noncopyable @@ -152,7 +156,7 @@ try { - ServerContext::DicomCacheLocker locker(context_, instance); + ServerContext::DicomCacheLocker locker(GetContext(), instance); ParsedDicomFile& original = locker.GetDicom(); originalHasher.reset(new DicomInstanceHasher(original.GetHasher())); @@ -171,6 +175,41 @@ modification_->Apply(*modified); + const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()); + + if (transcode_) + { + std::set syntaxes; + syntaxes.insert(transferSyntax_); + + IDicomTranscoder::DicomImage source; + source.AcquireParsed(*modified); // "modified" is invalid below this point + + IDicomTranscoder::DicomImage transcoded; + if (GetContext().Transcode(transcoded, source, syntaxes, true)) + { + modified.reset(transcoded.ReleaseAsParsedDicomFile()); + + // Fix the SOP instance UID in order the preserve the + // references between instance UIDs in the DICOM hierarchy + // (the UID might have changed in the case of lossy transcoding) + if (modified.get() == NULL || + modified->GetDcmtkObject().getDataset() == NULL || + !modified->GetDcmtkObject().getDataset()->putAndInsertString( + DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance; + modified.reset(source.ReleaseAsParsedDicomFile()); + } + } + + assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject())); + DicomInstanceToStore toStore; toStore.SetOrigin(origin_); toStore.SetParsedDicomFile(*modified); @@ -211,23 +250,32 @@ **/ std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + if (GetContext().Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { throw OrthancException(ErrorCode_CannotStoreInstance, "Error while storing a modified instance " + instance); } - assert(modifiedInstance == modifiedHasher.HashInstance()); + /** + * The assertion below will fail if automated transcoding to a + * lossy transfer syntax is enabled in the Orthanc core, and if + * the source instance is not in this transfer syntax. + **/ + // assert(modifiedInstance == modifiedHasher.HashInstance()); output_->Update(modifiedHasher); return true; } - - bool ResourceModificationJob::HandleTrailingStep() + + ResourceModificationJob::ResourceModificationJob(ServerContext& context) : + CleaningInstancesJob(context, true /* by default, keep source */), + modification_(new DicomModification), + isAnonymization_(false), + transcode_(false) { - throw OrthancException(ErrorCode_InternalError); } @@ -284,9 +332,64 @@ } + DicomTransferSyntax ResourceModificationJob::GetTransferSyntax() const + { + if (transcode_) + { + return transferSyntax_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void ResourceModificationJob::SetTranscode(DicomTransferSyntax syntax) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = true; + transferSyntax_ = syntax; + } + } + + + void ResourceModificationJob::SetTranscode(const std::string& transferSyntaxUid) + { + DicomTransferSyntax s; + if (LookupTransferSyntax(s, transferSyntaxUid)) + { + SetTranscode(s); + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Unknown transfer syntax UID: " + transferSyntaxUid); + } + } + + + void ResourceModificationJob::ClearTranscode() + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + transcode_ = false; + } + } + + void ResourceModificationJob::GetPublicContent(Json::Value& value) { - SetOfInstancesJob::GetPublicContent(value); + CleaningInstancesJob::GetPublicContent(value); value["IsAnonymization"] = isAnonymization_; @@ -294,33 +397,57 @@ { output_->Format(value); } + + if (transcode_) + { + value["Transcode"] = GetTransferSyntaxUid(transferSyntax_); + } } static const char* MODIFICATION = "Modification"; static const char* ORIGIN = "Origin"; static const char* IS_ANONYMIZATION = "IsAnonymization"; + static const char* TRANSCODE = "Transcode"; ResourceModificationJob::ResourceModificationJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), - context_(context) + CleaningInstancesJob(context, serialized, true /* by default, keep source */) { + assert(serialized.type() == Json::objectValue); + isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION); origin_ = DicomInstanceOrigin(serialized[ORIGIN]); modification_.reset(new DicomModification(serialized[MODIFICATION])); + + if (serialized.isMember(TRANSCODE)) + { + SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE)); + } + else + { + transcode_ = false; + } } bool ResourceModificationJob::Serialize(Json::Value& value) { - if (!SetOfInstancesJob::Serialize(value)) + if (!CleaningInstancesJob::Serialize(value)) { return false; } else { + assert(value.type() == Json::objectValue); + value[IS_ANONYMIZATION] = isAnonymization_; + + if (transcode_) + { + value[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_); + } + origin_.Serialize(value[ORIGIN]); Json::Value tmp; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/ResourceModificationJob.h --- a/OrthancServer/ServerJobs/ResourceModificationJob.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/ResourceModificationJob.h Wed May 20 16:42:44 2020 +0200 @@ -33,36 +33,31 @@ #pragma once -#include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../../Core/DicomParsing/DicomModification.h" #include "../DicomInstanceOrigin.h" +#include "CleaningInstancesJob.h" namespace Orthanc { class ServerContext; - class ResourceModificationJob : public SetOfInstancesJob + class ResourceModificationJob : public CleaningInstancesJob { private: class Output; - ServerContext& context_; std::unique_ptr modification_; boost::shared_ptr output_; bool isAnonymization_; DicomInstanceOrigin origin_; + bool transcode_; + DicomTransferSyntax transferSyntax_; protected: virtual bool HandleInstance(const std::string& instance); - virtual bool HandleTrailingStep(); - public: - ResourceModificationJob(ServerContext& context) : - context_(context), - isAnonymization_(false) - { - } + ResourceModificationJob(ServerContext& context); ResourceModificationJob(ServerContext& context, const Json::Value& serialized); @@ -87,6 +82,19 @@ return origin_; } + bool IsTranscode() const + { + return transcode_; + } + + DicomTransferSyntax GetTransferSyntax() const; + + void SetTranscode(DicomTransferSyntax syntax); + + void SetTranscode(const std::string& transferSyntaxUid); + + void ClearTranscode(); + virtual void Stop(JobStopReason reason) { } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/SplitStudyJob.cpp --- a/OrthancServer/ServerJobs/SplitStudyJob.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp Wed May 20 16:42:44 2020 +0200 @@ -67,6 +67,12 @@ bool SplitStudyJob::HandleInstance(const std::string& instance) { + if (!HasTrailingStep()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "AddTrailingStep() should have been called after AddSourceSeries()"); + } + /** * Retrieve the DICOM instance to be modified **/ @@ -75,7 +81,7 @@ try { - ServerContext::DicomCacheLocker locker(context_, instance); + ServerContext::DicomCacheLocker locker(GetContext(), instance); modified.reset(locker.GetDicom().Clone(true)); } catch (OrthancException&) @@ -138,7 +144,8 @@ toStore.SetParsedDicomFile(*modified); std::string modifiedInstance; - if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + if (GetContext().Store(modifiedInstance, toStore, + StoreInstanceMode_Default) != StoreStatus_Success) { LOG(ERROR) << "Error while storing a modified instance " << instance; return false; @@ -148,27 +155,9 @@ } - bool SplitStudyJob::HandleTrailingStep() - { - if (!keepSource_) - { - const size_t n = GetInstancesCount(); - - for (size_t i = 0; i < n; i++) - { - Json::Value tmp; - context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); - } - } - - return true; - } - - SplitStudyJob::SplitStudyJob(ServerContext& context, const std::string& sourceStudy) : - context_(context), - keepSource_(false), + CleaningInstancesJob(context, false /* by default, remove source instances */), sourceStudy_(sourceStudy), targetStudyUid_(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study)) { @@ -176,7 +165,7 @@ ResourceType type; - if (!context_.GetIndex().LookupResourceType(type, sourceStudy) || + if (!GetContext().GetIndex().LookupResourceType(type, sourceStudy) || type != ResourceType_Study) { throw OrthancException(ErrorCode_UnknownResource, @@ -212,7 +201,7 @@ { throw OrthancException(ErrorCode_BadSequenceOfCalls); } - else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study) || + else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study) || parent != sourceStudy_) { throw OrthancException(ErrorCode_UnknownResource, @@ -225,7 +214,7 @@ // Add all the instances of the series as to be processed std::list instances; - context_.GetIndex().GetChildren(instances, series); + GetContext().GetIndex().GetChildren(instances, series); for (std::list::const_iterator it = instances.begin(); it != instances.end(); ++it) @@ -236,17 +225,6 @@ } - void SplitStudyJob::SetKeepSource(bool keep) - { - if (IsStarted()) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - keepSource_ = keep; - } - - bool SplitStudyJob::LookupTargetSeriesUid(std::string& uid, const std::string& series) const { @@ -308,7 +286,7 @@ void SplitStudyJob::GetPublicContent(Json::Value& value) { - SetOfInstancesJob::GetPublicContent(value); + CleaningInstancesJob::GetPublicContent(value); if (!targetStudy_.empty()) { @@ -319,7 +297,6 @@ } - static const char* KEEP_SOURCE = "KeepSource"; static const char* SOURCE_STUDY = "SourceStudy"; static const char* TARGET_STUDY = "TargetStudy"; static const char* TARGET_STUDY_UID = "TargetStudyUID"; @@ -331,8 +308,8 @@ SplitStudyJob::SplitStudyJob(ServerContext& context, const Json::Value& serialized) : - SetOfInstancesJob(serialized), // (*) - context_(context) + CleaningInstancesJob(context, serialized, + false /* by default, remove source instances */) // (*) { if (!HasTrailingStep()) { @@ -342,7 +319,6 @@ Setup(); - keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE); sourceStudy_ = SerializationToolbox::ReadString(serialized, SOURCE_STUDY); targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY); targetStudyUid_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY_UID); @@ -355,13 +331,12 @@ bool SplitStudyJob::Serialize(Json::Value& target) { - if (!SetOfInstancesJob::Serialize(target)) + if (!CleaningInstancesJob::Serialize(target)) { return false; } else { - target[KEEP_SOURCE] = keepSource_; target[SOURCE_STUDY] = sourceStudy_; target[TARGET_STUDY] = targetStudy_; target[TARGET_STUDY_UID] = targetStudyUid_; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/ServerJobs/SplitStudyJob.h --- a/OrthancServer/ServerJobs/SplitStudyJob.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/ServerJobs/SplitStudyJob.h Wed May 20 16:42:44 2020 +0200 @@ -33,24 +33,22 @@ #pragma once -#include "../../Core/JobsEngine/SetOfInstancesJob.h" #include "../../Core/DicomFormat/DicomTag.h" #include "../DicomInstanceOrigin.h" +#include "CleaningInstancesJob.h" namespace Orthanc { class ServerContext; - class SplitStudyJob : public SetOfInstancesJob + class SplitStudyJob : public CleaningInstancesJob { private: typedef std::map SeriesUidMap; typedef std::map Replacements; - ServerContext& context_; std::set allowedTags_; - bool keepSource_; std::string sourceStudy_; std::string targetStudy_; std::string targetStudyUid_; @@ -66,8 +64,6 @@ protected: virtual bool HandleInstance(const std::string& instance); - virtual bool HandleTrailingStep(); - public: SplitStudyJob(ServerContext& context, const std::string& sourceStudy); @@ -92,13 +88,6 @@ void AddSourceSeries(const std::string& series); - bool IsKeepSource() const - { - return keepSource_; - } - - void SetKeepSource(bool keep); - bool LookupTargetSeriesUid(std::string& uid, const std::string& series) const; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/SliceOrdering.cpp --- a/OrthancServer/SliceOrdering.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/SliceOrdering.cpp Wed May 20 16:42:44 2020 +0200 @@ -320,7 +320,6 @@ if (instances_.size() <= 1) { // One single instance: It is sorted by default - sortedInstances_ = instances_; return true; } @@ -329,32 +328,25 @@ return false; } - sortedInstances_.clear(); - - // consider only the instances with a position and correctly oriented (if they have a normal) for (size_t i = 0; i < instances_.size(); i++) { assert(instances_[i] != NULL); - if (instances_[i]->HasPosition() && - (!instances_[i]->HasNormal() || - IsParallelOrOpposite(instances_[i]->GetNormal(), normal_))) + + if (!instances_[i]->HasPosition() || + (instances_[i]->HasNormal() && + !IsParallelOrOpposite(instances_[i]->GetNormal(), normal_))) { - sortedInstances_.push_back(instances_[i]); + return false; } } - if (sortedInstances_.size() == 0) - { - return false; - } + PositionComparator comparator(normal_); + std::sort(instances_.begin(), instances_.end(), comparator); - PositionComparator comparator(normal_); - std::sort(sortedInstances_.begin(), sortedInstances_.end(), comparator); - - float a = sortedInstances_[0]->ComputeRelativePosition(normal_); - for (size_t i = 1; i < sortedInstances_.size(); i++) + float a = instances_[0]->ComputeRelativePosition(normal_); + for (size_t i = 1; i < instances_.size(); i++) { - float b = sortedInstances_[i]->ComputeRelativePosition(normal_); + float b = instances_[i]->ComputeRelativePosition(normal_); if (std::fabs(b - a) <= 10.0f * std::numeric_limits::epsilon()) { @@ -376,38 +368,27 @@ if (instances_.size() <= 1) { // One single instance: It is sorted by default - sortedInstances_ = instances_; return true; } - sortedInstances_.clear(); - - // consider only the instances with an index for (size_t i = 0; i < instances_.size(); i++) { assert(instances_[i] != NULL); - if (instances_[i]->HasIndexInSeries()) + if (!instances_[i]->HasIndexInSeries()) { - sortedInstances_.push_back(instances_[i]); + return false; } } - if (sortedInstances_.size() == 0) // if we were not able to sort instances because none of them had an index, return all instances in a "random" order - { - sortedInstances_ = instances_; - } - else + std::sort(instances_.begin(), instances_.end(), IndexInSeriesComparator); + + for (size_t i = 1; i < instances_.size(); i++) { - std::sort(sortedInstances_.begin(), sortedInstances_.end(), IndexInSeriesComparator); - - for (size_t i = 1; i < sortedInstances_.size(); i++) + if (instances_[i - 1]->GetIndexInSeries() == instances_[i]->GetIndexInSeries()) { - if (sortedInstances_[i - 1]->GetIndexInSeries() == sortedInstances_[i]->GetIndexInSeries()) - { - // The current "IndexInSeries" occurs 2 times: Not a proper ordering - LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway"; - break; - } + // The current "IndexInSeries" occurs 2 times: Not a proper ordering + LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway"; + break; } } @@ -446,28 +427,28 @@ } - const std::string& SliceOrdering::GetSortedInstanceId(size_t index) const + const std::string& SliceOrdering::GetInstanceId(size_t index) const { - if (index >= sortedInstances_.size()) + if (index >= instances_.size()) { throw OrthancException(ErrorCode_ParameterOutOfRange); } else { - return sortedInstances_[index]->GetIdentifier(); + return instances_[index]->GetIdentifier(); } } - unsigned int SliceOrdering::GetSortedInstanceFramesCount(size_t index) const + unsigned int SliceOrdering::GetFramesCount(size_t index) const { - if (index >= sortedInstances_.size()) + if (index >= instances_.size()) { throw OrthancException(ErrorCode_ParameterOutOfRange); } else { - return sortedInstances_[index]->GetFramesCount(); + return instances_[index]->GetFramesCount(); } } @@ -478,9 +459,9 @@ result["Type"] = (isVolume_ ? "Volume" : "Sequence"); Json::Value tmp = Json::arrayValue; - for (size_t i = 0; i < GetSortedInstancesCount(); i++) + for (size_t i = 0; i < GetInstancesCount(); i++) { - tmp.append(GetBasePath(ResourceType_Instance, GetSortedInstanceId(i)) + "/file"); + tmp.append(GetBasePath(ResourceType_Instance, GetInstanceId(i)) + "/file"); } result["Dicom"] = tmp; @@ -488,18 +469,18 @@ Json::Value slicesShort = Json::arrayValue; tmp.clear(); - for (size_t i = 0; i < GetSortedInstancesCount(); i++) + for (size_t i = 0; i < GetInstancesCount(); i++) { - std::string base = GetBasePath(ResourceType_Instance, GetSortedInstanceId(i)); - for (size_t j = 0; j < GetSortedInstanceFramesCount(i); j++) + std::string base = GetBasePath(ResourceType_Instance, GetInstanceId(i)); + for (size_t j = 0; j < GetFramesCount(i); j++) { tmp.append(base + "/frames/" + boost::lexical_cast(j)); } Json::Value tmp2 = Json::arrayValue; - tmp2.append(GetSortedInstanceId(i)); + tmp2.append(GetInstanceId(i)); tmp2.append(0); - tmp2.append(GetSortedInstanceFramesCount(i)); + tmp2.append(GetFramesCount(i)); slicesShort.append(tmp2); } diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/SliceOrdering.h --- a/OrthancServer/SliceOrdering.h Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/SliceOrdering.h Wed May 20 16:42:44 2020 +0200 @@ -51,8 +51,7 @@ std::string seriesId_; bool hasNormal_; Vector normal_; - std::vector instances_; // this vector owns the instances - std::vector sortedInstances_; // this vectore references the instances of instances_ + std::vector instances_; bool isVolume_; static bool ComputeNormal(Vector& normal, @@ -78,14 +77,14 @@ ~SliceOrdering(); - size_t GetSortedInstancesCount() const + size_t GetInstancesCount() const { - return sortedInstances_.size(); + return instances_.size(); } - const std::string& GetSortedInstanceId(size_t index) const; + const std::string& GetInstanceId(size_t index) const; - unsigned int GetSortedInstanceFramesCount(size_t index) const; + unsigned int GetFramesCount(size_t index) const; void Format(Json::Value& result) const; }; diff -r fe0e4ef52a72 -r 6e14f2da7c7e OrthancServer/main.cpp --- a/OrthancServer/main.cpp Wed May 06 08:40:48 2020 +0200 +++ b/OrthancServer/main.cpp Wed May 20 16:42:44 2020 +0200 @@ -88,7 +88,7 @@ toStore.SetJson(dicomJson); std::string id; - context_.Store(id, toStore); + context_.Store(id, toStore, StoreInstanceMode_Default); } } }; @@ -1289,7 +1289,6 @@ HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", "")); - DicomUserConnection::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10)); DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10)); maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10); @@ -1309,7 +1308,7 @@ context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true)); // New option in Orthanc 1.4.2 - context.GetIndex().SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false)); + context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false)); try { diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Wed May 20 16:42:44 2020 +0200 @@ -62,7 +62,6 @@ #include "../../Core/OrthancException.h" #include "../../Core/SerializationToolbox.h" #include "../../Core/Toolbox.h" -#include "../../OrthancServer/DefaultDicomImageDecoder.h" #include "../../OrthancServer/OrthancConfiguration.h" #include "../../OrthancServer/OrthancFindRequestHandler.h" #include "../../OrthancServer/Search/HierarchicalMatcher.h" @@ -71,12 +70,13 @@ #include "PluginsEnumerations.h" #include "PluginsJob.h" -#include +#include #include #include #define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary" + namespace Orthanc { static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, @@ -330,9 +330,11 @@ class DicomWebBinaryFormatter : public DicomWebJsonVisitor::IBinaryFormatter { private: - OrthancPluginDicomWebBinaryCallback callback_; - DicomWebJsonVisitor::BinaryMode currentMode_; - std::string currentBulkDataUri_; + OrthancPluginDicomWebBinaryCallback oldCallback_; + OrthancPluginDicomWebBinaryCallback2 newCallback_; // New in Orthanc 1.7.0 + void* newPayload_; // New in Orthanc 1.7.0 + DicomWebJsonVisitor::BinaryMode currentMode_; + std::string currentBulkDataUri_; static void Setter(OrthancPluginDicomWebNode* node, OrthancPluginDicomWebBinaryMode mode, @@ -366,8 +368,18 @@ } public: - DicomWebBinaryFormatter(const _OrthancPluginEncodeDicomWeb& parameters) : - callback_(parameters.callback) + DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback callback) : + oldCallback_(callback), + newCallback_(NULL), + newPayload_(NULL) + { + } + + DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) : + oldCallback_(NULL), + newCallback_(callback), + newPayload_(payload) { } @@ -377,7 +389,8 @@ const DicomTag& tag, ValueRepresentation vr) { - if (callback_ == NULL) + if (oldCallback_ == NULL && + newCallback_ == NULL) { return DicomWebJsonVisitor::BinaryMode_InlineBinary; } @@ -398,20 +411,70 @@ currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore; - callback_(reinterpret_cast(this), - DicomWebBinaryFormatter::Setter, - static_cast(parentTags.size()), - (empty ? NULL : &groups[0]), - (empty ? NULL : &elements[0]), - (empty ? NULL : &indexes[0]), - tag.GetGroup(), - tag.GetElement(), - Plugins::Convert(vr)); + if (oldCallback_ != NULL) + { + oldCallback_(reinterpret_cast(this), + DicomWebBinaryFormatter::Setter, + static_cast(parentTags.size()), + (empty ? NULL : &groups[0]), + (empty ? NULL : &elements[0]), + (empty ? NULL : &indexes[0]), + tag.GetGroup(), + tag.GetElement(), + Plugins::Convert(vr)); + } + else + { + assert(newCallback_ != NULL); + newCallback_(reinterpret_cast(this), + DicomWebBinaryFormatter::Setter, + static_cast(parentTags.size()), + (empty ? NULL : &groups[0]), + (empty ? NULL : &elements[0]), + (empty ? NULL : &indexes[0]), + tag.GetGroup(), + tag.GetElement(), + Plugins::Convert(vr), + newPayload_); + } bulkDataUri = currentBulkDataUri_; return currentMode_; } } + + void Apply(char** target, + bool isJson, + ParsedDicomFile& dicom) + { + DicomWebJsonVisitor visitor; + visitor.SetFormatter(*this); + + dicom.Apply(visitor); + + std::string s; + + if (isJson) + { + s = visitor.GetResult().toStyledString(); + } + else + { + visitor.FormatXml(s); + } + + *target = CopyString(s); + } + + + void Apply(char** target, + bool isJson, + const void* dicom, + size_t dicomSize) + { + ParsedDicomFile parsed(dicom, dicomSize); + Apply(target, isJson, parsed); + } }; } @@ -827,6 +890,7 @@ typedef std::list IncomingHttpRequestFilters2; typedef std::list IncomingDicomInstanceFilters; typedef std::list DecodeImageCallbacks; + typedef std::list TranscoderCallbacks; typedef std::list JobsUnserializers; typedef std::list RefreshMetricsCallbacks; typedef std::list StorageCommitmentScpCallbacks; @@ -841,6 +905,7 @@ OrthancPluginFindCallback findCallback_; OrthancPluginWorklistCallback worklistCallback_; DecodeImageCallbacks decodeImageCallbacks_; + TranscoderCallbacks transcoderCallbacks_; JobsUnserializers jobsUnserializers_; _OrthancPluginMoveCallback moveCallbacks_; IncomingHttpRequestFilters incomingHttpRequestFilters_; @@ -855,7 +920,7 @@ boost::recursive_mutex changeCallbackMutex_; boost::mutex findCallbackMutex_; boost::mutex worklistCallbackMutex_; - boost::mutex decodeImageCallbackMutex_; + boost::shared_mutex decoderTranscoderMutex_; // Changed from "boost::mutex" in Orthanc 1.7.0 boost::mutex jobsUnserializersMutex_; boost::mutex refreshMetricsMutex_; boost::mutex storageCommitmentScpMutex_; @@ -1761,19 +1826,110 @@ } + class OrthancPlugins::IDicomInstance : public boost::noncopyable + { + public: + virtual ~IDicomInstance() + { + } + + virtual bool CanBeFreed() const = 0; + + virtual const DicomInstanceToStore& GetInstance() const = 0; + }; + + + class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance + { + private: + const DicomInstanceToStore& instance_; + + public: + DicomInstanceFromCallback(const DicomInstanceToStore& instance) : + instance_(instance) + { + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return false; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance + { + private: + std::string buffer_; + DicomInstanceToStore instance_; + + public: + DicomInstanceFromBuffer(const void* buffer, + size_t size) + { + buffer_.assign(reinterpret_cast(buffer), size); + instance_.SetBuffer(buffer_.empty() ? NULL : buffer_.c_str(), buffer_.size()); + instance_.SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + + class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance + { + private: + std::unique_ptr parsed_; + DicomInstanceToStore instance_; + + public: + DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) : + parsed_(transcoded.ReleaseAsParsedDicomFile()) + { + instance_.SetParsedDicomFile(*parsed_); + instance_.SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + + virtual bool CanBeFreed() const ORTHANC_OVERRIDE + { + return true; + } + + virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE + { + return instance_; + }; + }; + + void OrthancPlugins::SignalStoredInstance(const std::string& instanceId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) { + DicomInstanceFromCallback wrapped(instance); + boost::recursive_mutex::scoped_lock lock(pimpl_->storedCallbackMutex_); for (PImpl::OnStoredCallbacks::const_iterator callback = pimpl_->onStoredCallbacks_.begin(); callback != pimpl_->onStoredCallbacks_.end(); ++callback) { - OrthancPluginErrorCode error = (*callback) - (reinterpret_cast(&instance), - instanceId.c_str()); + OrthancPluginErrorCode error = (*callback) ( + reinterpret_cast(&wrapped), + instanceId.c_str()); if (error != OrthancPluginErrorCode_Success) { @@ -1787,14 +1943,15 @@ bool OrthancPlugins::FilterIncomingInstance(const DicomInstanceToStore& instance, const Json::Value& simplified) { + DicomInstanceFromCallback wrapped(instance); + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); for (PImpl::IncomingDicomInstanceFilters::const_iterator filter = pimpl_->incomingDicomInstanceFilters_.begin(); filter != pimpl_->incomingDicomInstanceFilters_.end(); ++filter) { - int32_t allowed = (*filter) ( - reinterpret_cast(&instance)); + int32_t allowed = (*filter) (reinterpret_cast(&wrapped)); if (allowed == 0) { @@ -1954,7 +2111,7 @@ const _OrthancPluginDecodeImageCallback& p = *reinterpret_cast(parameters); - boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + boost::unique_lock lock(pimpl_->decoderTranscoderMutex_); pimpl_->decodeImageCallbacks_.push_back(p.callback); LOG(INFO) << "Plugin has registered a callback to decode DICOM images (" @@ -1962,6 +2119,19 @@ } + void OrthancPlugins::RegisterTranscoderCallback(const void* parameters) + { + const _OrthancPluginTranscoderCallback& p = + *reinterpret_cast(parameters); + + boost::unique_lock lock(pimpl_->decoderTranscoderMutex_); + + pimpl_->transcoderCallbacks_.push_back(p.callback); + LOG(INFO) << "Plugin has registered a callback to transcode DICOM images (" + << pimpl_->transcoderCallbacks_.size() << " transcoder(s) now active)"; + } + + void OrthancPlugins::RegisterJobsUnserializer(const void* parameters) { const _OrthancPluginJobsUnserializer& p = @@ -2451,14 +2621,19 @@ } - static void AccessDicomInstance(_OrthancPluginService service, - const void* parameters) + void OrthancPlugins::AccessDicomInstance(_OrthancPluginService service, + const void* parameters) { const _OrthancPluginAccessDicomInstance& p = *reinterpret_cast(parameters); + if (p.instance == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + const DicomInstanceToStore& instance = - *reinterpret_cast(p.instance); + reinterpret_cast(p.instance)->GetInstance(); switch (service) { @@ -2523,6 +2698,10 @@ *p.resultInt64 = instance.HasPixelData(); return; + case _OrthancPluginService_GetInstanceFramesCount: // New in Orthanc 1.7.0 + *p.resultInt64 = instance.GetParsedDicomFile().GetFramesCount(); + return; + default: throw OrthancException(ErrorCode_InternalError); } @@ -2592,6 +2771,11 @@ // Images returned to plugins are assumed to be writeable. If the // input image is read-only, we return a copy so that it can be modified. + if (image.get() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + if (image->IsReadOnly()) { std::unique_ptr copy(new Image(image->GetFormat(), image->GetWidth(), image->GetHeight(), false)); @@ -2606,6 +2790,114 @@ } + void OrthancPlugins::AccessDicomInstance2(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginAccessDicomInstance2& p = + *reinterpret_cast(parameters); + + if (p.instance == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + const DicomInstanceToStore& instance = + reinterpret_cast(p.instance)->GetInstance(); + + switch (service) + { + case _OrthancPluginService_GetInstanceFramesCount: + *p.targetUint32 = instance.GetParsedDicomFile().GetFramesCount(); + return; + + case _OrthancPluginService_GetInstanceRawFrame: + { + if (p.targetBuffer == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + p.targetBuffer->data = NULL; + p.targetBuffer->size = 0; + + MimeType mime; + std::string frame; + instance.GetParsedDicomFile().GetRawFrame(frame, mime, p.frameIndex); + CopyToMemoryBuffer(*p.targetBuffer, frame); + return; + } + + case _OrthancPluginService_GetInstanceDecodedFrame: + { + if (p.targetImage == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + std::unique_ptr decoded; + { + PImpl::ServerContextLock lock(*pimpl_); + decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex)); + } + + *(p.targetImage) = ReturnImage(decoded); + return; + } + + case _OrthancPluginService_SerializeDicomInstance: + { + if (p.targetBuffer == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + p.targetBuffer->data = NULL; + p.targetBuffer->size = 0; + + std::string serialized; + instance.GetParsedDicomFile().SaveToMemoryBuffer(serialized); + CopyToMemoryBuffer(*p.targetBuffer, serialized); + return; + } + + case _OrthancPluginService_GetInstanceAdvancedJson: + { + if (p.targetStringToFree == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + Json::Value json; + instance.GetParsedDicomFile().DatasetToJson( + json, Plugins::Convert(p.format), + static_cast(p.flags), p.maxStringLength); + + Json::FastWriter writer; + *p.targetStringToFree = CopyString(writer.write(json)); + return; + } + + case _OrthancPluginService_GetInstanceDicomWebJson: + case _OrthancPluginService_GetInstanceDicomWebXml: + { + if (p.targetStringToFree == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DicomWebBinaryFormatter formatter(p.dicomWebCallback, p.dicomWebPayload); + formatter.Apply(p.targetStringToFree, + (service == _OrthancPluginService_GetInstanceDicomWebJson), + instance.GetParsedDicomFile()); + return; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + void OrthancPlugins::UncompressImage(const void* parameters) { const _OrthancPluginUncompressImage& p = *reinterpret_cast(parameters); @@ -3481,6 +3773,16 @@ AccessDicomInstance(service, parameters); return true; + case _OrthancPluginService_GetInstanceFramesCount: + case _OrthancPluginService_GetInstanceRawFrame: + case _OrthancPluginService_GetInstanceDecodedFrame: + case _OrthancPluginService_SerializeDicomInstance: + case _OrthancPluginService_GetInstanceAdvancedJson: + case _OrthancPluginService_GetInstanceDicomWebJson: + case _OrthancPluginService_GetInstanceDicomWebXml: + AccessDicomInstance2(service, parameters); + return true; + case _OrthancPluginService_SetGlobalProperty: { const _OrthancPluginGlobalProperty& p = @@ -3998,28 +4300,23 @@ const _OrthancPluginEncodeDicomWeb& p = *reinterpret_cast(parameters); - DicomWebBinaryFormatter formatter(p); - - DicomWebJsonVisitor visitor; - visitor.SetFormatter(formatter); - - { - ParsedDicomFile dicom(p.dicom, p.dicomSize); - dicom.Apply(visitor); - } - - std::string s; - - if (service == _OrthancPluginService_EncodeDicomWebJson) - { - s = visitor.GetResult().toStyledString(); - } - else - { - visitor.FormatXml(s); - } - - *p.target = CopyString(s); + DicomWebBinaryFormatter formatter(p.callback); + formatter.Apply(p.target, + (service == _OrthancPluginService_EncodeDicomWebJson), + p.dicom, p.dicomSize); + return true; + } + + case _OrthancPluginService_EncodeDicomWebJson2: + case _OrthancPluginService_EncodeDicomWebXml2: + { + const _OrthancPluginEncodeDicomWeb2& p = + *reinterpret_cast(parameters); + + DicomWebBinaryFormatter formatter(p.callback, p.payload); + formatter.Apply(p.target, + (service == _OrthancPluginService_EncodeDicomWebJson2), + p.dicom, p.dicomSize); return true; } @@ -4027,6 +4324,98 @@ GetTagName(parameters); return true; + case _OrthancPluginService_CreateDicomInstance: + { + const _OrthancPluginCreateDicomInstance& p = + *reinterpret_cast(parameters); + *(p.target) = reinterpret_cast( + new DicomInstanceFromBuffer(p.buffer, p.size)); + return true; + } + + case _OrthancPluginService_FreeDicomInstance: + { + const _OrthancPluginFreeDicomInstance& p = + *reinterpret_cast(parameters); + + if (p.dicom != NULL) + { + IDicomInstance* obj = reinterpret_cast(p.dicom); + + if (obj->CanBeFreed()) + { + delete obj; + } + else + { + throw OrthancException(ErrorCode_Plugin, "Cannot free a DICOM instance provided to a callback"); + } + } + + return true; + } + + case _OrthancPluginService_TranscodeDicomInstance: + { + const _OrthancPluginCreateDicomInstance& p = + *reinterpret_cast(parameters); + + DicomTransferSyntax transferSyntax; + if (p.transferSyntax == NULL || + !LookupTransferSyntax(transferSyntax, p.transferSyntax)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "Unsupported transfer syntax: " + + std::string(p.transferSyntax == NULL ? "(null)" : p.transferSyntax)); + } + else + { + std::set syntaxes; + syntaxes.insert(transferSyntax); + + IDicomTranscoder::DicomImage source; + source.SetExternalBuffer(p.buffer, p.size); + + IDicomTranscoder::DicomImage transcoded; + bool success; + + { + PImpl::ServerContextLock lock(*pimpl_); + success = lock.GetContext().Transcode( + transcoded, source, syntaxes, true /* allow new sop */); + } + + if (success) + { + *(p.target) = reinterpret_cast( + new DicomInstanceFromTranscoded(transcoded)); + return true; + } + else + { + throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode image"); + } + } + } + + case _OrthancPluginService_CreateMemoryBuffer: + { + const _OrthancPluginCreateMemoryBuffer& p = + *reinterpret_cast(parameters); + + p.target->size = p.size; + + if (p.size == 0) + { + p.target->data = NULL; + } + else + { + p.target->data = malloc(p.size); + } + + return true; + } + default: return false; } @@ -4080,6 +4469,10 @@ RegisterDecodeImageCallback(parameters); return true; + case _OrthancPluginService_RegisterTranscoderCallback: + RegisterTranscoderCallback(parameters); + return true; + case _OrthancPluginService_RegisterJobsUnserializer: RegisterJobsUnserializer(parameters); return true; @@ -4464,16 +4857,23 @@ bool OrthancPlugins::HasCustomImageDecoder() { - boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); return !pimpl_->decodeImageCallbacks_.empty(); } - ImageAccessor* OrthancPlugins::DecodeUnsafe(const void* dicom, - size_t size, - unsigned int frame) + bool OrthancPlugins::HasCustomTranscoder() { - boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); + return !pimpl_->transcoderCallbacks_.empty(); + } + + + ImageAccessor* OrthancPlugins::Decode(const void* dicom, + size_t size, + unsigned int frame) + { + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); for (PImpl::DecodeImageCallbacks::const_iterator decoder = pimpl_->decodeImageCallbacks_.begin(); @@ -4490,25 +4890,6 @@ return NULL; } - - ImageAccessor* OrthancPlugins::Decode(const void* dicom, - size_t size, - unsigned int frame) - { - ImageAccessor* result = DecodeUnsafe(dicom, size, frame); - - if (result != NULL) - { - return result; - } - else - { - LOG(INFO) << "The installed image decoding plugins cannot handle an image, fallback to the built-in decoder"; - DefaultDicomImageDecoder defaultDecoder; - return defaultDecoder.Decode(dicom, size, frame); - } - } - bool OrthancPlugins::IsAllowed(HttpMethod method, const char* uri, @@ -4793,4 +5174,81 @@ return NULL; } + + + class MemoryBufferRaii : public boost::noncopyable + { + private: + OrthancPluginMemoryBuffer buffer_; + + public: + MemoryBufferRaii() + { + buffer_.size = 0; + buffer_.data = NULL; + } + + ~MemoryBufferRaii() + { + if (buffer_.size != 0) + { + free(buffer_.data); + } + } + + OrthancPluginMemoryBuffer* GetObject() + { + return &buffer_; + } + + void ToString(std::string& target) const + { + target.resize(buffer_.size); + + if (buffer_.size != 0) + { + memcpy(&target[0], buffer_.data, buffer_.size); + } + } + }; + + + bool OrthancPlugins::TranscodeBuffer(std::string& target, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) + { + boost::shared_lock lock(pimpl_->decoderTranscoderMutex_); + + if (pimpl_->transcoderCallbacks_.empty()) + { + return NULL; + } + + std::vector uids; + uids.reserve(allowedSyntaxes.size()); + for (std::set::const_iterator + it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it) + { + uids.push_back(GetTransferSyntaxUid(*it)); + } + + for (PImpl::TranscoderCallbacks::const_iterator + transcoder = pimpl_->transcoderCallbacks_.begin(); + transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder) + { + MemoryBufferRaii a; + + if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0], + static_cast(uids.size()), allowNewSopInstanceUid) == + OrthancPluginErrorCode_Success) + { + a.ToString(target); + return true; + } + } + + return false; + } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Engine/OrthancPlugins.h --- a/Plugins/Engine/OrthancPlugins.h Wed May 06 08:40:48 2020 +0200 +++ b/Plugins/Engine/OrthancPlugins.h Wed May 20 16:42:44 2020 +0200 @@ -56,6 +56,7 @@ #include "../../Core/DicomNetworking/IFindRequestHandlerFactory.h" #include "../../Core/DicomNetworking/IMoveRequestHandlerFactory.h" #include "../../Core/DicomNetworking/IWorklistRequestHandlerFactory.h" +#include "../../Core/DicomParsing/MemoryBufferTranscoder.h" #include "../../Core/FileStorage/IStorageArea.h" #include "../../Core/HttpServer/IHttpHandler.h" #include "../../Core/HttpServer/IIncomingHttpRequestFilter.h" @@ -82,7 +83,8 @@ public IIncomingHttpRequestFilter, public IFindRequestHandlerFactory, public IMoveRequestHandlerFactory, - public IStorageCommitmentFactory + public IStorageCommitmentFactory, + public MemoryBufferTranscoder { private: class PImpl; @@ -94,6 +96,10 @@ class HttpClientChunkedRequest; class HttpClientChunkedAnswer; class HttpServerChunkedReader; + class IDicomInstance; + class DicomInstanceFromCallback; + class DicomInstanceFromBuffer; + class DicomInstanceFromTranscoded; void RegisterRestCallback(const void* parameters, bool lock); @@ -118,6 +124,8 @@ void RegisterDecodeImageCallback(const void* parameters); + void RegisterTranscoderCallback(const void* parameters); + void RegisterJobsUnserializer(const void* parameters); void RegisterIncomingHttpRequestFilter(const void* parameters); @@ -155,6 +163,12 @@ void LookupResource(_OrthancPluginService service, const void* parameters); + void AccessDicomInstance(_OrthancPluginService service, + const void* parameters); + + void AccessDicomInstance2(_OrthancPluginService service, + const void* parameters); + void SendHttpStatusCode(const void* parameters); void SendHttpStatus(const void* parameters); @@ -223,6 +237,14 @@ _OrthancPluginService service, const void* parameters); + protected: + // From "MemoryBufferTranscoder" + virtual bool TranscodeBuffer(std::string& target, + const void* buffer, + size_t size, + const std::set& allowedSyntaxes, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + public: OrthancPlugins(); @@ -250,7 +272,7 @@ virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE; virtual void SignalStoredInstance(const std::string& instanceId, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, const Json::Value& simplifiedTags) ORTHANC_OVERRIDE; virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance, @@ -305,12 +327,7 @@ bool HasCustomImageDecoder(); - // Contrarily to "Decode()", this method does not fallback to the - // builtin image decoder, if no installed custom decoder can - // handle the image (it returns NULL in this case). - ImageAccessor* DecodeUnsafe(const void* dicom, - size_t size, - unsigned int frame); + bool HasCustomTranscoder(); virtual ImageAccessor* Decode(const void* dicom, size_t size, diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 06 08:40:48 2020 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 20 16:42:44 2020 +0200 @@ -28,6 +28,7 @@ * - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback(). * - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback(). * - Possibly register a callback to filter incoming DICOM instance using OrthancPluginRegisterIncomingDicomInstanceFilter(). + * - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback(). * -# void OrthancPluginFinalize(): * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -64,6 +65,9 @@ * * @defgroup Orthanc Orthanc * @brief Functions to access the content of the Orthanc server. + * + * @defgroup DicomInstance DicomInstance + * @brief Functions to access DICOM images that are managed by the Orthanc core. **/ @@ -124,8 +128,8 @@ #endif #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 -#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 6 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 1 +#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 7 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -437,8 +441,11 @@ _OrthancPluginService_SetMetricsValue = 31, _OrthancPluginService_EncodeDicomWebJson = 32, _OrthancPluginService_EncodeDicomWebXml = 33, - _OrthancPluginService_ChunkedHttpClient = 34, /* New in Orthanc 1.5.7 */ - _OrthancPluginService_GetTagName = 35, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_ChunkedHttpClient = 34, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_GetTagName = 35, /* New in Orthanc 1.5.7 */ + _OrthancPluginService_EncodeDicomWebJson2 = 36, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_EncodeDicomWebXml2 = 37, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_CreateMemoryBuffer = 38, /* New in Orthanc 1.7.0 */ /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -456,7 +463,8 @@ _OrthancPluginService_RegisterChunkedRestCallback = 1012, /* New in Orthanc 1.5.7 */ _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013, _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014, - + _OrthancPluginService_RegisterTranscoderCallback = 1015, /* New in Orthanc 1.7.0 */ + /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, _OrthancPluginService_CompressAndAnswerPngImage = 2001, /* Unused as of Orthanc 0.9.4 */ @@ -502,7 +510,17 @@ _OrthancPluginService_GetInstanceOrigin = 4007, _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008, _OrthancPluginService_HasInstancePixelData = 4009, - + _OrthancPluginService_CreateDicomInstance = 4010, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_FreeDicomInstance = 4011, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceFramesCount = 4012, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceRawFrame = 4013, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDecodedFrame = 4014, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_TranscodeDicomInstance = 4015, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_SerializeDicomInstance = 4016, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceAdvancedJson = 4017, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDicomWebJson = 4018, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_GetInstanceDicomWebXml = 4019, /* New in Orthanc 1.7.0 */ + /* Services for plugins implementing a database back-end */ _OrthancPluginService_RegisterDatabaseBackend = 5000, _OrthancPluginService_DatabaseAnswer = 5001, @@ -1009,7 +1027,8 @@ /** - * @brief Opaque structure that represents a DICOM instance received by Orthanc. + * @brief Opaque structure that represents a DICOM instance that is managed by the Orthanc core. + * @ingroup DicomInstance **/ typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance; @@ -1609,6 +1628,45 @@ /** + * @brief Callback executed to encode a binary tag in DICOMweb. + * + * Signature of a callback function that is called by Orthanc + * whenever a DICOM tag that contains a binary value must be written + * to a JSON or XML node, while a DICOMweb document is being + * generated. The value representation (VR) of the DICOM tag can be + * OB, OD, OF, OL, OW, or UN. + * + * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml() + * @param node The node being generated, as provided by Orthanc. + * @param setter The setter to be used to encode the content of the node. If + * the setter is not called, the binary tag is not written to the output document. + * @param levelDepth The depth of the node in the DICOM hierarchy of sequences. + * This parameter gives the number of elements in the "levelTagGroup", + * "levelTagElement", and "levelIndex" arrays. + * @param levelTagGroup The group of the parent DICOM tags in the hierarchy. + * @param levelTagElement The element of the parent DICOM tags in the hierarchy. + * @param levelIndex The index of the node in the parent sequences of the hierarchy. + * @param tagGroup The group of the DICOM tag of interest. + * @param tagElement The element of the DICOM tag of interest. + * @param vr The value representation of the binary DICOM node. + * @param payload The user payload. + * @ingroup Callbacks + **/ + typedef void (*OrthancPluginDicomWebBinaryCallback2) ( + OrthancPluginDicomWebNode* node, + OrthancPluginDicomWebSetBinaryNode setter, + uint32_t levelDepth, + const uint16_t* levelTagGroup, + const uint16_t* levelTagElement, + const uint32_t* levelIndex, + uint16_t tagGroup, + uint16_t tagElement, + OrthancPluginValueRepresentation vr, + void* payload); + + + + /** * @brief Data structure that contains information about the Orthanc core. **/ typedef struct _OrthancPluginContext_t @@ -2715,7 +2773,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The AET if success, NULL if error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet( OrthancPluginContext* context, @@ -2748,7 +2806,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The size of the file, -1 in case of error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize( OrthancPluginContext* context, @@ -2781,7 +2839,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The pointer to the DICOM data, NULL in case of error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData( OrthancPluginContext* context, @@ -2817,7 +2875,7 @@ * @param instance The instance of interest. * @return The NULL value in case of error, or a string containing the JSON file. * This string must be freed by OrthancPluginFreeString(). - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson( OrthancPluginContext* context, @@ -2855,7 +2913,7 @@ * @param instance The instance of interest. * @return The NULL value in case of error, or a string containing the JSON file. * This string must be freed by OrthancPluginFreeString(). - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson( OrthancPluginContext* context, @@ -2894,7 +2952,7 @@ * @param instance The instance of interest. * @param metadata The metadata of interest. * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE int OrthancPluginHasInstanceMetadata( OrthancPluginContext* context, @@ -2935,7 +2993,7 @@ * returned string belongs to the instance object and must NOT be * deallocated. Please make a copy of the string if you wish to access * it later. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata( OrthancPluginContext* context, @@ -5107,7 +5165,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instance The instance of interest. * @return The origin of the instance. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin( OrthancPluginContext* context, @@ -5183,8 +5241,11 @@ /** * @brief Register a callback to handle the decoding of DICOM images. * - * This function registers a custom callback to the decoding of - * DICOM images, replacing the built-in decoder of Orthanc. + * This function registers a custom callback to decode DICOM images, + * extending the built-in decoder of Orthanc that uses + * DCMTK. Starting with Orthanc 1.7.0, the exact behavior is + * affected by the configuration option + * "BuiltinDecoderTranscoderOrder" of Orthanc. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param callback The callback. @@ -5316,6 +5377,7 @@ * @param frameIndex The index of the frame of interest in a multi-frame image. * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). * @ingroup Images + * @see OrthancPluginGetInstanceDecodedFrame() **/ ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginDecodeDicomImage( OrthancPluginContext* context, @@ -6791,6 +6853,7 @@ * @see OrthancPluginCreateDicom() * @return The NULL value in case of error, or the JSON document. This string must * be freed by OrthancPluginFreeString(). + * @deprecated OrthancPluginEncodeDicomWebJson2() * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson( @@ -6829,9 +6892,10 @@ * @param dicom Pointer to the DICOM instance. * @param dicomSize Size of the DICOM instance. * @param callback Callback to set the value of the binary tags. - * @return The NULL value in case of error, or the JSON document. This string must + * @return The NULL value in case of error, or the XML document. This string must * be freed by OrthancPluginFreeString(). * @see OrthancPluginCreateDicom() + * @deprecated OrthancPluginEncodeDicomWebXml2() * @ingroup Toolbox **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml( @@ -6861,6 +6925,104 @@ + typedef struct + { + char** target; + const void* dicom; + uint32_t dicomSize; + OrthancPluginDicomWebBinaryCallback2 callback; + void* payload; + } _OrthancPluginEncodeDicomWeb2; + + /** + * @brief Convert a DICOM instance to DICOMweb JSON. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb JSON representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the JSON document. This string must + * be freed by OrthancPluginFreeString(). + * @see OrthancPluginCreateDicom() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson2( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb2 params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + params.payload = payload; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson2, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb XML. + * + * This function converts a memory buffer containing a DICOM instance, + * into its DICOMweb XML representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom Pointer to the DICOM instance. + * @param dicomSize Size of the DICOM instance. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the XML document. This string must + * be freed by OrthancPluginFreeString(). + * @see OrthancPluginCreateDicom() + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2( + OrthancPluginContext* context, + const void* dicom, + uint32_t dicomSize, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginEncodeDicomWeb2 params; + params.target = ⌖ + params.dicom = dicom; + params.dicomSize = dicomSize; + params.callback = callback; + params.payload = payload; + + if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml2, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** * @brief Callback executed when a HTTP header is received during a chunked transfer. * @@ -7476,7 +7638,7 @@ * @param instance The instance of interest. * @return The NULL value in case of error, or a string containing the * transfer syntax UID. This string must be freed by OrthancPluginFreeString(). - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid( OrthancPluginContext* context, @@ -7511,7 +7673,7 @@ * @param instance The instance of interest. * @return "1" if the DICOM instance contains pixel data, or "0" if * the tag is missing, or "-1" in the case of an error. - * @ingroup Callbacks + * @ingroup DicomInstance **/ ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData( OrthancPluginContext* context, @@ -7538,6 +7700,486 @@ } + + + + + typedef struct + { + OrthancPluginDicomInstance** target; + const void* buffer; + uint32_t size; + const char* transferSyntax; + } _OrthancPluginCreateDicomInstance; + + /** + * @brief Parse a DICOM instance. + * + * This function parses a memory buffer that contains a DICOM + * file. The function returns a new pointer to a data structure that + * is managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM instance. + * @param size The size of the memory buffer. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance( + OrthancPluginContext* context, + const void* buffer, + uint32_t size) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginCreateDicomInstance params; + params.target = ⌖ + params.buffer = buffer; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_CreateDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + typedef struct + { + OrthancPluginDicomInstance* dicom; + } _OrthancPluginFreeDicomInstance; + + /** + * @brief Free a DICOM instance. + * + * This function frees a DICOM instance that was parsed using + * OrthancPluginCreateDicomInstance(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param dicom The DICOM instance. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE void OrthancPluginFreeDicomInstance( + OrthancPluginContext* context, + OrthancPluginDicomInstance* dicom) + { + _OrthancPluginFreeDicomInstance params; + params.dicom = dicom; + + context->InvokeService(context, _OrthancPluginService_FreeDicomInstance, ¶ms); + } + + + typedef struct + { + uint32_t* targetUint32; + OrthancPluginMemoryBuffer* targetBuffer; + OrthancPluginImage** targetImage; + char** targetStringToFree; + const OrthancPluginDicomInstance* instance; + uint32_t frameIndex; + OrthancPluginDicomToJsonFormat format; + OrthancPluginDicomToJsonFlags flags; + uint32_t maxStringLength; + OrthancPluginDicomWebBinaryCallback2 dicomWebCallback; + void* dicomWebPayload; + } _OrthancPluginAccessDicomInstance2; + + /** + * @brief Get the number of frames in a DICOM instance. + * + * This function returns the number of frames that are part of a + * DICOM image managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The number of frames (will be zero in the case of an error). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance) + { + uint32_t count; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetUint32 = &count; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceFramesCount, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return 0; + } + else + { + return count; + } + } + + + /** + * @brief Get the raw content of a frame in a DICOM instance. + * + * This function returns a memory buffer containing the raw content + * of a frame in a DICOM instance that is managed by the Orthanc + * core. This is notably useful for compressed transfer syntaxes, as + * it gives access to the embedded files (such as JPEG, JPEG-LS or + * JPEG2k). The Orthanc core transparently reassembles the fragments + * to extract the raw frame. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param instance The instance of interest. + * @param frameIndex The index of the frame of interest. + * @return 0 if success, or the error code if failure. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginDicomInstance* instance, + uint32_t frameIndex) + { + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetBuffer = target; + params.instance = instance; + params.frameIndex = frameIndex; + + return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, ¶ms); + } + + + /** + * @brief Decode one frame from a DICOM instance. + * + * This function decodes one frame of a DICOM image that is managed + * by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @param frameIndex The index of the frame of interest. + * @return The uncompressed image. It must be freed with OrthancPluginFreeImage(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + uint32_t frameIndex) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetImage = ⌖ + params.instance = instance; + params.frameIndex = frameIndex; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDecodedFrame, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Parse and transcode a DICOM instance. + * + * This function parses a memory buffer that contains a DICOM file, + * then transcodes it to the given transfer syntax. The function + * returns a new pointer to a data structure that is managed by the + * Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The memory buffer containing the DICOM instance. + * @param size The size of the memory buffer. + * @param transferSyntax The transfer syntax UID for the transcoding. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance( + OrthancPluginContext* context, + const void* buffer, + uint32_t size, + const char* transferSyntax) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginCreateDicomInstance params; + params.target = ⌖ + params.buffer = buffer; + params.size = size; + params.transferSyntax = transferSyntax; + + if (context->InvokeService(context, _OrthancPluginService_TranscodeDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + /** + * @brief Writes a DICOM instance to a memory buffer. + * + * This function returns a memory buffer containing the + * serialization of a DICOM instance that is managed by the Orthanc + * core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param instance The instance of interest. + * @return 0 if success, or the error code if failure. + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginDicomInstance* instance) + { + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetBuffer = target; + params.instance = instance; + + return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, ¶ms); + } + + + /** + * @brief Format a DICOM memory buffer as a JSON string. + * + * This function takes as DICOM instance managed by the Orthanc + * core, and outputs a JSON string representing the tags of this + * DICOM file. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param format The output format. + * @param flags Flags governing the output. + * @param maxStringLength The maximum length of a field. Too long fields will + * be output as "null". The 0 value means no maximum length. + * @return The NULL value if the case of an error, or the JSON + * string. This string must be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + * @see OrthancPluginDicomBufferToJson + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomToJsonFormat format, + OrthancPluginDicomToJsonFlags flags, + uint32_t maxStringLength) + { + char* result = NULL; + + _OrthancPluginAccessDicomInstance2 params; + memset(¶ms, 0, sizeof(params)); + params.targetStringToFree = &result; + params.instance = instance; + params.format = format; + params.flags = flags; + params.maxStringLength = maxStringLength; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceAdvancedJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb JSON. + * + * This function converts a DICOM instance that is managed by the + * Orthanc core, into its DICOMweb JSON representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the JSON document. This string must + * be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + params.targetStringToFree = ⌖ + params.instance = instance; + params.dicomWebCallback = callback; + params.dicomWebPayload = payload; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebJson, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Convert a DICOM instance to DICOMweb XML. + * + * This function converts a DICOM instance that is managed by the + * Orthanc core, into its DICOMweb XML representation. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The DICOM instance of interest. + * @param callback Callback to set the value of the binary tags. + * @param payload User payload. + * @return The NULL value in case of error, or the XML document. This string must + * be freed by OrthancPluginFreeString(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml( + OrthancPluginContext* context, + const OrthancPluginDicomInstance* instance, + OrthancPluginDicomWebBinaryCallback2 callback, + void* payload) + { + char* target = NULL; + + _OrthancPluginAccessDicomInstance2 params; + params.targetStringToFree = ⌖ + params.instance = instance; + params.dicomWebCallback = callback; + params.dicomWebPayload = payload; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebXml, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Signature of a callback function to transcode a DICOM instance. + * @param transcoded Target memory buffer. It must be allocated by the + * plugin using OrthancPluginCreateMemoryBuffer(). + * @param buffer Memory buffer containing the source DICOM instance. + * @param size Size of the source memory buffer. + * @param allowedSyntaxes A C array of possible transfer syntaxes UIDs for the + * result of the transcoding. The plugin must choose by itself the + * transfer syntax that will be used for the resulting DICOM image. + * @param countSyntaxes The number of transfer syntaxes that are contained + * in the "allowedSyntaxes" array. + * @param allowNewSopInstanceUid Whether the transcoding plugin can select + * a transfer syntax that will change the SOP instance UID (or, in other + * terms, whether the plugin can transcode using lossy compression). + * @return 0 if success (i.e. image successfully transcoded and stored into + * "transcoded"), or the error code if failure. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginTranscoderCallback) ( + OrthancPluginMemoryBuffer* transcoded /* out */, + const void* buffer, + uint64_t size, + const char* const* allowedSyntaxes, + uint32_t countSyntaxes, + uint8_t allowNewSopInstanceUid); + + + typedef struct + { + OrthancPluginTranscoderCallback callback; + } _OrthancPluginTranscoderCallback; + + /** + * @brief Register a callback to handle the transcoding of DICOM images. + * + * This function registers a custom callback to transcode DICOM + * images, extending the built-in transcoder of Orthanc that uses + * DCMTK. The exact behavior is affected by the configuration option + * "BuiltinDecoderTranscoderOrder" of Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterTranscoderCallback( + OrthancPluginContext* context, + OrthancPluginTranscoderCallback callback) + { + _OrthancPluginTranscoderCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterTranscoderCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + uint32_t size; + } _OrthancPluginCreateMemoryBuffer; + + /** + * @brief Create a memory buffer. + * + * This function creates a memory buffer that is managed by the + * Orthanc core. The main use case of this function is for plugins + * that act as DICOM transcoders. + * + * Your plugin should never call "free()" on the resulting memory + * buffer, as the C library that is used by the plugin is in general + * not the same as the one used by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param size Size of the memory buffer to be created. + * @return 0 if success, or the error code if failure. + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + uint32_t size) + { + _OrthancPluginCreateMemoryBuffer params; + params.target = target; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer, ¶ms); + } + + #ifdef __cplusplus } #endif diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/Common/OrthancPluginCppWrapper.cpp --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed May 06 08:40:48 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Wed May 20 16:42:44 2020 +0200 @@ -40,6 +40,11 @@ #include +#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0) +static const OrthancPluginErrorCode OrthancPluginErrorCode_NullPointer = OrthancPluginErrorCode_Plugin; +#endif + + namespace OrthancPlugins { static OrthancPluginContext* globalContext_ = NULL; @@ -125,6 +130,28 @@ } +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + MemoryBuffer::MemoryBuffer(const void* buffer, + size_t size) + { + uint32_t s = static_cast(size); + if (static_cast(s) != size) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) != + OrthancPluginErrorCode_Success) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory); + } + else + { + memcpy(buffer_.data, buffer, size); + } + } +#endif + + void MemoryBuffer::Clear() { if (buffer_.data != NULL) @@ -1045,7 +1072,7 @@ } - const void* OrthancImage::GetBuffer() const + void* OrthancImage::GetBuffer() const { CheckImageAvailable(); return OrthancPluginGetImageBuffer(GetGlobalContext(), image_); @@ -1094,6 +1121,14 @@ } + OrthancPluginImage* OrthancImage::Release() + { + CheckImageAvailable(); + OrthancPluginImage* tmp = image_; + image_ = NULL; + return tmp; + } + #if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) : @@ -3177,4 +3212,184 @@ delete reinterpret_cast(rawHandler); } #endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + DicomInstance::DicomInstance(const OrthancPluginDicomInstance* instance) : + toFree_(false), + instance_(instance) + { + } +#else + DicomInstance::DicomInstance(OrthancPluginDicomInstance* instance) : + toFree_(false), + instance_(instance) + { + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance::DicomInstance(const void* buffer, + size_t size) : + toFree_(true), + instance_(OrthancPluginCreateDicomInstance(GetGlobalContext(), buffer, size)) + { + if (instance_ == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer); + } + } +#endif + + + DicomInstance::~DicomInstance() + { +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + if (toFree_ && + instance_ != NULL) + { + OrthancPluginFreeDicomInstance( + GetGlobalContext(), const_cast(instance_)); + } +#endif + } + + + std::string DicomInstance::GetRemoteAet() const + { + const char* s = OrthancPluginGetInstanceRemoteAet(GetGlobalContext(), instance_); + if (s == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return std::string(s); + } + } + + + void DicomInstance::GetJson(Json::Value& target) const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceJson(GetGlobalContext(), instance_)); + s.ToJson(target); + } + + + void DicomInstance::GetSimplifiedJson(Json::Value& target) const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceSimplifiedJson(GetGlobalContext(), instance_)); + s.ToJson(target); + } + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + std::string DicomInstance::GetTransferSyntaxUid() const + { + OrthancString s; + s.Assign(OrthancPluginGetInstanceTransferSyntaxUid(GetGlobalContext(), instance_)); + + std::string result; + s.ToString(result); + return result; + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + bool DicomInstance::HasPixelData() const + { + int32_t result = OrthancPluginHasInstancePixelData(GetGlobalContext(), instance_); + if (result < 0) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return (result != 0); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void DicomInstance::GetRawFrame(std::string& target, + unsigned int frameIndex) const + { + MemoryBuffer buffer; + OrthancPluginErrorCode code = OrthancPluginGetInstanceRawFrame( + GetGlobalContext(), *buffer, instance_, frameIndex); + + if (code == OrthancPluginErrorCode_Success) + { + buffer.ToString(target); + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + OrthancImage* DicomInstance::GetDecodedFrame(unsigned int frameIndex) const + { + OrthancPluginImage* image = OrthancPluginGetInstanceDecodedFrame( + GetGlobalContext(), instance_, frameIndex); + + if (image == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + return new OrthancImage(image); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void DicomInstance::Serialize(std::string& target) const + { + MemoryBuffer buffer; + OrthancPluginErrorCode code = OrthancPluginSerializeDicomInstance( + GetGlobalContext(), *buffer, instance_); + + if (code == OrthancPluginErrorCode_Success) + { + buffer.ToString(target); + } + else + { + ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code); + } + } +#endif + + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance* DicomInstance::Transcode(const void* buffer, + size_t size, + const std::string& transferSyntax) + { + OrthancPluginDicomInstance* instance = OrthancPluginTranscodeDicomInstance( + GetGlobalContext(), buffer, size, transferSyntax.c_str()); + + if (instance == NULL) + { + ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin); + } + else + { + boost::movelib::unique_ptr result(new DicomInstance(instance)); + result->toFree_ = true; + return result.release(); + } + } +#endif } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/Common/OrthancPluginCppWrapper.h --- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed May 06 08:40:48 2020 +0200 +++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h Wed May 20 16:42:44 2020 +0200 @@ -47,6 +47,13 @@ +/** + * The definition of ORTHANC_PLUGINS_VERSION_IS_ABOVE below is for + * backward compatibility with Orthanc SDK <= 1.3.0. + * + * $ hg diff -r Orthanc-1.3.0:Orthanc-1.3.1 ../../../Plugins/Include/orthanc/OrthancCPlugin.h + * + **/ #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) #define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ @@ -139,6 +146,13 @@ public: MemoryBuffer(); +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + // This constructor makes a copy of the given buffer in the memory + // handled by the Orthanc core + MemoryBuffer(const void* buffer, + size_t size); +#endif + ~MemoryBuffer() { Clear(); @@ -379,8 +393,7 @@ uint32_t width, uint32_t height, uint32_t pitch, - void* buffer - ); + void* buffer); ~OrthancImage() { @@ -405,7 +418,7 @@ unsigned int GetPitch() const; - const void* GetBuffer() const; + void* GetBuffer() const; const OrthancPluginImage* GetObject() const { @@ -421,6 +434,10 @@ void AnswerJpegImage(OrthancPluginRestOutput* output, uint8_t quality) const; + + void* GetWriteableBuffer(); + + OrthancPluginImage* Release(); }; @@ -1128,4 +1145,86 @@ static void Destructor(void* rawHandler); }; #endif + + + class DicomInstance : public boost::noncopyable + { + private: + bool toFree_; + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + const OrthancPluginDicomInstance* instance_; +#else + OrthancPluginDicomInstance* instance_; +#endif + + public: +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + DicomInstance(const OrthancPluginDicomInstance* instance); +#else + DicomInstance(OrthancPluginDicomInstance* instance); +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + DicomInstance(const void* buffer, + size_t size); +#endif + + ~DicomInstance(); + + std::string GetRemoteAet() const; + + const void* GetBuffer() const + { + return OrthancPluginGetInstanceData(GetGlobalContext(), instance_); + } + + size_t GetSize() const + { + return OrthancPluginGetInstanceSize(GetGlobalContext(), instance_); + } + + void GetJson(Json::Value& target) const; + + void GetSimplifiedJson(Json::Value& target) const; + + OrthancPluginInstanceOrigin GetOrigin() const + { + return OrthancPluginGetInstanceOrigin(GetGlobalContext(), instance_); + } + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + std::string GetTransferSyntaxUid() const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1) + bool HasPixelData() const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + unsigned int GetFramesCount() const + { + return OrthancPluginGetInstanceFramesCount(GetGlobalContext(), instance_); + } +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void GetRawFrame(std::string& target, + unsigned int frameIndex) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + OrthancImage* GetDecodedFrame(unsigned int frameIndex) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + void Serialize(std::string& target) const; +#endif + +#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0) + static DicomInstance* Transcode(const void* buffer, + size_t size, + const std::string& transferSyntax); +#endif + }; } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/CMakeLists.txt --- a/Plugins/Samples/GdcmDecoder/CMakeLists.txt Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 2.8) - -project(GdcmDecoder) - -SET(GDCM_DECODER_VERSION "0.0" CACHE STRING "Version of the plugin") -SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") -SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") - -set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) -include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake) -include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) - -find_package(GDCM REQUIRED) -if (GDCM_FOUND) - include(${GDCM_USE_FILE}) - set(GDCM_LIBRARIES gdcmCommon gdcmMSFF) -else(GDCM_FOUND) - message(FATAL_ERROR "Cannot find GDCM, did you set GDCM_DIR?") -endif(GDCM_FOUND) - -add_definitions(-DGDCM_DECODER_VERSION="${GDCM_DECODER_VERSION}") - -add_library(GdcmDecoder SHARED - ${BOOST_SOURCES} - GdcmDecoderCache.cpp - GdcmImageDecoder.cpp - OrthancImageWrapper.cpp - Plugin.cpp - ) - -target_link_libraries(GdcmDecoder ${GDCM_LIBRARIES}) diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp --- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "GdcmDecoderCache.h" - -#include "../../../Core/Compatibility.h" -#include "OrthancImageWrapper.h" - -namespace OrthancPlugins -{ - std::string GdcmDecoderCache::ComputeMd5(OrthancPluginContext* context, - const void* dicom, - size_t size) - { - std::string result; - - char* md5 = OrthancPluginComputeMd5(context, dicom, size); - - if (md5 == NULL) - { - throw std::runtime_error("Cannot compute MD5 hash"); - } - - bool ok = false; - try - { - result.assign(md5); - ok = true; - } - catch (...) - { - } - - OrthancPluginFreeString(context, md5); - - if (!ok) - { - throw std::runtime_error("Not enough memory"); - } - else - { - return result; - } - } - - - OrthancImageWrapper* GdcmDecoderCache::Decode(OrthancPluginContext* context, - const void* dicom, - const uint32_t size, - uint32_t frameIndex) - { - std::string md5 = ComputeMd5(context, dicom, size); - - // First check whether the previously decoded image is the same - // as this one - { - boost::mutex::scoped_lock lock(mutex_); - - if (decoder_.get() != NULL && - size_ == size && - md5_ == md5) - { - // This is the same image: Reuse the previous decoding - return new OrthancImageWrapper(context, decoder_->Decode(context, frameIndex)); - } - } - - // This is not the same image - std::unique_ptr decoder(new GdcmImageDecoder(dicom, size)); - std::unique_ptr image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex))); - - { - // Cache the newly created decoder for further use - boost::mutex::scoped_lock lock(mutex_); - decoder_.reset(decoder.release()); - size_ = size; - md5_ = md5; - } - - return image.release(); - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h --- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../../../Core/Compatibility.h" -#include "GdcmImageDecoder.h" -#include "OrthancImageWrapper.h" - -#include - - -namespace OrthancPlugins -{ - class GdcmDecoderCache : public boost::noncopyable - { - private: - boost::mutex mutex_; - std::unique_ptr decoder_; - size_t size_; - std::string md5_; - - static std::string ComputeMd5(OrthancPluginContext* context, - const void* dicom, - size_t size); - - public: - GdcmDecoderCache() : size_(0) - { - } - - OrthancImageWrapper* Decode(OrthancPluginContext* context, - const void* dicom, - const uint32_t size, - uint32_t frameIndex); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp --- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,409 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "GdcmImageDecoder.h" - -#include "../../../Core/Compatibility.h" -#include "OrthancImageWrapper.h" - -#include -#include -#include -#include -#include -#include -#include - - -namespace OrthancPlugins -{ - struct GdcmImageDecoder::PImpl - { - const void* dicom_; - size_t size_; - - gdcm::ImageReader reader_; - std::unique_ptr lut_; - std::unique_ptr photometric_; - std::unique_ptr interleaved_; - std::string decoded_; - - PImpl(const void* dicom, - size_t size) : - dicom_(dicom), - size_(size) - { - } - - - const gdcm::DataSet& GetDataSet() const - { - return reader_.GetFile().GetDataSet(); - } - - - const gdcm::Image& GetImage() const - { - if (interleaved_.get() != NULL) - { - return interleaved_->GetOutput(); - } - - if (lut_.get() != NULL) - { - return lut_->GetOutput(); - } - - if (photometric_.get() != NULL) - { - return photometric_->GetOutput(); - } - - return reader_.GetImage(); - } - - - void Decode() - { - // Change photometric interpretation or apply LUT, if required - { - const gdcm::Image& image = GetImage(); - if (image.GetPixelFormat().GetSamplesPerPixel() == 1 && - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::PALETTE_COLOR) - { - lut_.reset(new gdcm::ImageApplyLookupTable()); - lut_->SetInput(image); - if (!lut_->Apply()) - { - throw std::runtime_error( "GDCM cannot apply the lookup table"); - } - } - else if (image.GetPixelFormat().GetSamplesPerPixel() == 1) - { - if (image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME1 && - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2) - { - photometric_.reset(new gdcm::ImageChangePhotometricInterpretation()); - photometric_->SetInput(image); - photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::MONOCHROME2); - if (!photometric_->Change() || - GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::MONOCHROME2) - { - throw std::runtime_error("GDCM cannot change the photometric interpretation"); - } - } - } - else - { - if (image.GetPixelFormat().GetSamplesPerPixel() == 3 && - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB && - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_FULL && - (image.GetTransferSyntax() != gdcm::TransferSyntax::JPEG2000Lossless || - image.GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::YBR_RCT)) - { - photometric_.reset(new gdcm::ImageChangePhotometricInterpretation()); - photometric_->SetInput(image); - photometric_->SetPhotometricInterpretation(gdcm::PhotometricInterpretation::RGB); - if (!photometric_->Change() || - GetImage().GetPhotometricInterpretation() != gdcm::PhotometricInterpretation::RGB) - { - throw std::runtime_error("GDCM cannot change the photometric interpretation"); - } - } - } - } - - // Possibly convert planar configuration to interleaved - { - const gdcm::Image& image = GetImage(); - if (image.GetPlanarConfiguration() != 0 && - image.GetPixelFormat().GetSamplesPerPixel() != 1) - { - interleaved_.reset(new gdcm::ImageChangePlanarConfiguration()); - interleaved_->SetInput(image); - if (!interleaved_->Change() || - GetImage().GetPlanarConfiguration() != 0) - { - throw std::runtime_error("GDCM cannot change the planar configuration to interleaved"); - } - } - } - } - }; - - GdcmImageDecoder::GdcmImageDecoder(const void* dicom, - size_t size) : - pimpl_(new PImpl(dicom, size)) - { - // Setup a stream to the memory buffer - using namespace boost::iostreams; - basic_array_source source(reinterpret_cast(dicom), size); - stream > stream(source); - - // Parse the DICOM instance using GDCM - pimpl_->reader_.SetStream(stream); - if (!pimpl_->reader_.Read()) - { - throw std::runtime_error("Bad file format"); - } - - pimpl_->Decode(); - } - - - OrthancPluginPixelFormat GdcmImageDecoder::GetFormat() const - { - const gdcm::Image& image = pimpl_->GetImage(); - - if (image.GetPixelFormat().GetSamplesPerPixel() == 1 && - (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME1 || - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::MONOCHROME2)) - { - switch (image.GetPixelFormat()) - { - case gdcm::PixelFormat::UINT16: - return OrthancPluginPixelFormat_Grayscale16; - - case gdcm::PixelFormat::INT16: - return OrthancPluginPixelFormat_SignedGrayscale16; - - case gdcm::PixelFormat::UINT8: - return OrthancPluginPixelFormat_Grayscale8; - - default: - throw std::runtime_error("Unsupported pixel format"); - } - } - else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 && - (image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::RGB || - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_FULL || - image.GetPhotometricInterpretation() == gdcm::PhotometricInterpretation::YBR_RCT)) - { - switch (image.GetPixelFormat()) - { - case gdcm::PixelFormat::UINT8: - return OrthancPluginPixelFormat_RGB24; - - case gdcm::PixelFormat::UINT16: -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1) - return OrthancPluginPixelFormat_RGB48; -#else - throw std::runtime_error("RGB48 pixel format is only supported if compiled against Orthanc SDK >= 1.3.1"); -#endif - - default: - break; - } - } - - throw std::runtime_error("Unsupported pixel format"); - } - - - unsigned int GdcmImageDecoder::GetWidth() const - { - return pimpl_->GetImage().GetColumns(); - } - - - unsigned int GdcmImageDecoder::GetHeight() const - { - return pimpl_->GetImage().GetRows(); - } - - - unsigned int GdcmImageDecoder::GetFramesCount() const - { - return pimpl_->GetImage().GetDimension(2); - } - - - size_t GdcmImageDecoder::GetBytesPerPixel(OrthancPluginPixelFormat format) - { - switch (format) - { - case OrthancPluginPixelFormat_Grayscale8: - return 1; - - case OrthancPluginPixelFormat_Grayscale16: - case OrthancPluginPixelFormat_SignedGrayscale16: - return 2; - - case OrthancPluginPixelFormat_RGB24: - return 3; - -#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 3, 1) - case OrthancPluginPixelFormat_RGB48: - return 6; -#endif - - default: - throw std::runtime_error("Unsupport pixel format"); - } - } - - static void ConvertYbrToRgb(uint8_t rgb[3], - const uint8_t ybr[3]) - { - // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2 - // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion - - // TODO - Check out the outcome of Mathieu's discussion about - // truncation of YCbCr-to-RGB conversion: - // https://groups.google.com/forum/#!msg/comp.protocols.dicom/JHuGeyWbTz8/ARoTWrJzAQAJ - - const float Y = ybr[0]; - const float Cb = ybr[1]; - const float Cr = ybr[2]; - - const float result[3] = { - Y + 1.402f * (Cr - 128.0f), - Y - 0.344136f * (Cb - 128.0f) - 0.714136f * (Cr - 128.0f), - Y + 1.772f * (Cb - 128.0f) - }; - - for (uint8_t i = 0; i < 3 ; i++) - { - if (result[i] < 0) - { - rgb[i] = 0; - } - else if (result[i] > 255) - { - rgb[i] = 255; - } - else - { - rgb[i] = static_cast(result[i]); - } - } - } - - - static void FixPhotometricInterpretation(OrthancImageWrapper& image, - gdcm::PhotometricInterpretation interpretation) - { - switch (interpretation) - { - case gdcm::PhotometricInterpretation::MONOCHROME1: - case gdcm::PhotometricInterpretation::MONOCHROME2: - case gdcm::PhotometricInterpretation::RGB: - return; - - case gdcm::PhotometricInterpretation::YBR_FULL: - { - // Fix for Osimis issue WVB-319: Some images are not loading in US_MF - - uint32_t width = image.GetWidth(); - uint32_t height = image.GetHeight(); - uint32_t pitch = image.GetPitch(); - uint8_t* buffer = reinterpret_cast(image.GetBuffer()); - - if (image.GetFormat() != OrthancPluginPixelFormat_RGB24 || - pitch < 3 * width) - { - throw std::runtime_error("Internal error"); - } - - for (uint32_t y = 0; y < height; y++) - { - uint8_t* p = buffer + y * pitch; - for (uint32_t x = 0; x < width; x++, p += 3) - { - const uint8_t ybr[3] = { p[0], p[1], p[2] }; - uint8_t rgb[3]; - ConvertYbrToRgb(rgb, ybr); - p[0] = rgb[0]; - p[1] = rgb[1]; - p[2] = rgb[2]; - } - } - - return; - } - - default: - throw std::runtime_error("Unsupported output photometric interpretation"); - } - } - - - OrthancPluginImage* GdcmImageDecoder::Decode(OrthancPluginContext* context, - unsigned int frameIndex) const - { - unsigned int frames = GetFramesCount(); - unsigned int width = GetWidth(); - unsigned int height = GetHeight(); - OrthancPluginPixelFormat format = GetFormat(); - size_t bpp = GetBytesPerPixel(format); - - if (frameIndex >= frames) - { - throw std::runtime_error("Inexistent frame index"); - } - - std::string& decoded = pimpl_->decoded_; - OrthancImageWrapper target(context, format, width, height); - - if (width == 0 || - height == 0) - { - return target.Release(); - } - - if (decoded.empty()) - { - decoded.resize(pimpl_->GetImage().GetBufferLength()); - if (!pimpl_->GetImage().GetBuffer(&decoded[0])) - { - throw std::runtime_error("Image not properly decoded to a memory buffer"); - } - } - - const void* sourceBuffer = &decoded[0]; - - if (target.GetPitch() == bpp * width && - frames == 1) - { - assert(decoded.size() == target.GetPitch() * target.GetHeight()); - memcpy(target.GetBuffer(), sourceBuffer, decoded.size()); - } - else - { - size_t targetPitch = target.GetPitch(); - size_t sourcePitch = width * bpp; - - const char* a = &decoded[sourcePitch * height * frameIndex]; - char* b = target.GetBuffer(); - - for (uint32_t y = 0; y < height; y++) - { - memcpy(b, a, sourcePitch); - a += sourcePitch; - b += targetPitch; - } - } - - FixPhotometricInterpretation(target, pimpl_->GetImage().GetPhotometricInterpretation()); - - return target.Release(); - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h --- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include -#include -#include -#include - - -// This is for compatibility with Orthanc SDK <= 1.3.0 -#if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) -#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \ - (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major || \ - (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major && \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor || \ - (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor && \ - ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision)))) -#endif - - -namespace OrthancPlugins -{ - class GdcmImageDecoder : public boost::noncopyable - { - private: - struct PImpl; - boost::shared_ptr pimpl_; - - public: - GdcmImageDecoder(const void* dicom, - size_t size); - - OrthancPluginPixelFormat GetFormat() const; - - unsigned int GetWidth() const; - - unsigned int GetHeight() const; - - unsigned int GetFramesCount() const; - - static size_t GetBytesPerPixel(OrthancPluginPixelFormat format); - - OrthancPluginImage* Decode(OrthancPluginContext* context, - unsigned int frameIndex) const; - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/OrthancImageWrapper.cpp --- a/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "OrthancImageWrapper.h" - -#include - -namespace OrthancPlugins -{ - OrthancImageWrapper::OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginPixelFormat format, - uint32_t width, - uint32_t height) : - context_(context) - { - image_ = OrthancPluginCreateImage(context_, format, width, height); - if (image_ == NULL) - { - throw std::runtime_error("Cannot create an image"); - } - } - - - OrthancImageWrapper::OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginImage* image) : - context_(context), - image_(image) - { - if (image_ == NULL) - { - throw std::runtime_error("Invalid image returned by the core of Orthanc"); - } - } - - - - OrthancImageWrapper::~OrthancImageWrapper() - { - if (image_ != NULL) - { - OrthancPluginFreeImage(context_, image_); - } - } - - - OrthancPluginImage* OrthancImageWrapper::Release() - { - OrthancPluginImage* tmp = image_; - image_ = NULL; - return tmp; - } - - - uint32_t OrthancImageWrapper::GetWidth() - { - return OrthancPluginGetImageWidth(context_, image_); - } - - - uint32_t OrthancImageWrapper::GetHeight() - { - return OrthancPluginGetImageHeight(context_, image_); - } - - - uint32_t OrthancImageWrapper::GetPitch() - { - return OrthancPluginGetImagePitch(context_, image_); - } - - - OrthancPluginPixelFormat OrthancImageWrapper::GetFormat() - { - return OrthancPluginGetImagePixelFormat(context_, image_); - } - - - char* OrthancImageWrapper::GetBuffer() - { - return reinterpret_cast(OrthancPluginGetImageBuffer(context_, image_)); - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/OrthancImageWrapper.h --- a/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include - -#include "GdcmImageDecoder.h" - -namespace OrthancPlugins -{ - class OrthancImageWrapper - { - private: - OrthancPluginContext* context_; - OrthancPluginImage* image_; - - public: - OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginPixelFormat format, - uint32_t width, - uint32_t height); - - OrthancImageWrapper(OrthancPluginContext* context, - OrthancPluginImage* image); // Takes ownership - - ~OrthancImageWrapper(); - - OrthancPluginContext* GetContext() - { - return context_; - } - - OrthancPluginImage* Release(); - - uint32_t GetWidth(); - - uint32_t GetHeight(); - - uint32_t GetPitch(); - - OrthancPluginPixelFormat GetFormat(); - - char* GetBuffer(); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/Plugin.cpp --- a/Plugins/Samples/GdcmDecoder/Plugin.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../../../Core/Compatibility.h" -#include "GdcmDecoderCache.h" -#include "OrthancImageWrapper.h" - -#include - -static OrthancPluginContext* context_ = NULL; -static OrthancPlugins::GdcmDecoderCache cache_; - - -static OrthancPluginErrorCode DecodeImageCallback(OrthancPluginImage** target, - const void* dicom, - const uint32_t size, - uint32_t frameIndex) -{ - try - { - std::unique_ptr image; - -#if 0 - // Do not use the cache - OrthancPlugins::GdcmImageDecoder decoder(dicom, size); - image.reset(new OrthancPlugins::OrthancImageWrapper(context_, decoder.Decode(context_, frameIndex))); -#else - image.reset(cache_.Decode(context_, dicom, size, frameIndex)); -#endif - - *target = image->Release(); - - return OrthancPluginErrorCode_Success; - } - catch (std::runtime_error& e) - { - *target = NULL; - - std::string s = "Cannot decode image using GDCM: " + std::string(e.what()); - OrthancPluginLogInfo(context_, s.c_str()); - return OrthancPluginErrorCode_Plugin; - } -} - - - -extern "C" -{ - ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) - { - context_ = context; - OrthancPluginLogWarning(context_, "Initializing the advanced decoder of medical images using GDCM"); - - - /* Check the version of the Orthanc core */ - if (OrthancPluginCheckVersion(context_) == 0) - { - char info[1024]; - sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin", - context_->orthancVersion, - ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER, - ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER, - ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER); - OrthancPluginLogError(context_, info); - return -1; - } - - OrthancPluginSetDescription(context_, "Advanced decoder of medical images using GDCM."); - OrthancPluginRegisterDecodeImageCallback(context_, DecodeImageCallback); - - return 0; - } - - - ORTHANC_PLUGINS_API void OrthancPluginFinalize() - { - } - - - ORTHANC_PLUGINS_API const char* OrthancPluginGetName() - { - return "gdcm-decoder"; - } - - - ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() - { - return GDCM_DECODER_VERSION; - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Plugins/Samples/GdcmDecoder/README --- a/Plugins/Samples/GdcmDecoder/README Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -This sample shows how to replace the decoder of DICOM images that is -built in Orthanc, by the GDCM library. - -A production-ready version of this sample, is available in the -offical Web viewer plugin: -http://www.orthanc-server.com/static.php?page=web-viewer diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/CMake/DcmtkConfiguration.cmake --- a/Resources/CMake/DcmtkConfiguration.cmake Wed May 06 08:40:48 2020 +0200 +++ b/Resources/CMake/DcmtkConfiguration.cmake Wed May 20 16:42:44 2020 +0200 @@ -36,6 +36,14 @@ ) endif() + if (ENABLE_DCMTK_TRANSCODING) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimgle/libsrc DCMTK_SOURCES) + AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimage/libsrc DCMTK_SOURCES) + include_directories( + ${DCMTK_SOURCES_DIR}/dcmimage/include + ) + endif() + if (ENABLE_DCMTK_JPEG) AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc DCMTK_SOURCES) AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8 DCMTK_SOURCES) @@ -56,17 +64,21 @@ ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8/jaricom.c ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12/jaricom.c ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg24/jaricom.c + ) - # Disable support for encoding JPEG (modification in Orthanc 1.0.1) - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djcodece.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsv1.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencbas.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencpro.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djenclol.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencode.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencext.cc - ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsps.cc - ) + if (NOT ENABLE_DCMTK_TRANSCODING) + list(REMOVE_ITEM DCMTK_SOURCES + # Disable support for encoding JPEG (modification in Orthanc 1.0.1) + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djcodece.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsv1.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencbas.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencpro.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djenclol.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencode.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencext.cc + ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djencsps.cc + ) + endif() endif() @@ -78,15 +90,18 @@ ${DCMTK_SOURCES_DIR}/dcmjpls/include ${DCMTK_SOURCES_DIR}/dcmjpls/libcharls ) - list(REMOVE_ITEM DCMTK_SOURCES - ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djcodece.cc - - # Disable support for encoding JPEG-LS (modification in Orthanc 1.0.1) - ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djencode.cc - ) list(APPEND DCMTK_SOURCES ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djrplol.cc ) + + if (NOT ENABLE_DCMTK_TRANSCODING) + list(REMOVE_ITEM DCMTK_SOURCES + ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djcodece.cc + + # Disable support for encoding JPEG-LS (modification in Orthanc 1.0.1) + ${DCMTK_SOURCES_DIR}/dcmjpls/libsrc/djencode.cc + ) + endif() endif() diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed May 06 08:40:48 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed May 20 16:42:44 2020 +0200 @@ -484,10 +484,9 @@ ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociation.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociationParameters.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomControlUserConnection.cpp - ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp - ${ORTHANC_ROOT}/Core/DicomNetworking/DicomUserConnection.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/CommandDispatcher.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/FindScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/MoveScp.cpp @@ -502,6 +501,11 @@ # New in Orthanc 1.6.0 if (ENABLE_DCMTK_TRANSCODING) add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=1) + list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL + ${ORTHANC_ROOT}/Core/DicomParsing/DcmtkTranscoder.cpp + ${ORTHANC_ROOT}/Core/DicomParsing/IDicomTranscoder.cpp + ${ORTHANC_ROOT}/Core/DicomParsing/MemoryBufferTranscoder.cpp + ) else() add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=0) endif() diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/CMake/OrthancFrameworkParameters.cmake --- a/Resources/CMake/OrthancFrameworkParameters.cmake Wed May 06 08:40:48 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkParameters.cmake Wed May 20 16:42:44 2020 +0200 @@ -17,7 +17,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "6") +set(ORTHANC_API_VERSION "7") ##################################################################### diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Configuration.json --- a/Resources/Configuration.json Wed May 06 08:40:48 2020 +0200 +++ b/Resources/Configuration.json Wed May 20 16:42:44 2020 +0200 @@ -209,9 +209,17 @@ * registered remote SCU modalities. Starting with Orthanc 1.5.0, * it is possible to specify which DICOM commands are allowed, * separately for each remote modality, using the syntax - * below. The "AllowEcho" (resp. "AllowStore") option only has an - * effect respectively if global option "DicomAlwaysAllowEcho" - * (resp. "DicomAlwaysAllowStore") is set to false. + * below. + * + * The "AllowEcho" (resp. "AllowStore") option only has an effect + * respectively if global option "DicomAlwaysAllowEcho" + * (resp. "DicomAlwaysAllowStore") is set to "false". + * + * Starting with Orthanc 1.7.0, "AllowTranscoding" can be used to + * disable the transcoding to uncompressed transfer syntaxes if + * the remote modality doesn't support compressed transfer + * syntaxes. This option only has an effect if global option + * "EnableTranscoding" is set to "true". **/ //"untrusted" : { // "AET" : "ORTHANC", @@ -222,7 +230,8 @@ // "AllowFind" : false, // "AllowMove" : false, // "AllowStore" : true, - // "AllowStorageCommitment" : false // new in 1.6.0 + // "AllowStorageCommitment" : false, // new in 1.6.0 + // "AllowTranscoding" : true // new in 1.7.0 //} }, @@ -532,5 +541,35 @@ // Maximum number of storage commitment reports (i.e. received from // remote modalities) to be kept in memory (new in Orthanc 1.6.0). - "StorageCommitmentReportsSize" : 100 + "StorageCommitmentReportsSize" : 100, + + // Whether Orthanc transcodes DICOM files to an uncompressed + // transfer syntax over the DICOM protocol, if the remote modality + // does not support compressed transfer syntaxes (new in Orthanc 1.7.0). + "TranscodeDicomProtocol" : true, + + // If some plugin to decode/transcode DICOM instances is installed, + // this option specifies whether the built-in decoder/transcoder of + // Orthanc (that uses DCMTK) is applied before or after the plugins, + // or is not applied at all (new in Orthanc 1.7.0). The allowed + // values for this option are "After" (default value, corresponding + // to the behavior of Orthanc <= 1.6.1), "Before", or "Disabled". + "BuiltinDecoderTranscoderOrder" : "After", + + // If this option is set, Orthanc will transparently transcode any + // incoming DICOM instance to the given transfer syntax before + // storing it into its database. Beware that this might result in + // high CPU usage (if transcoding to some compressed transfer + // syntax), or in higher disk consumption (if transcoding to an + // uncompressed syntax). Also, beware that transcoding to a transfer + // syntax with lossy compression (notably JPEG) will change the + // "SOPInstanceUID" DICOM tag, and thus the Orthanc identifier at + // the instance level, which might break external workflow. + /** + "IngestTranscoding" : "1.2.840.10008.1.2", + **/ + + // The compression level that is used when transcoding to one of the + // lossy/JPEG transfer syntaxes (integer between 1 and 100). + "DicomLossyTranscodingQuality" : 90 } diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/DicomTransferSyntaxes.json --- a/Resources/DicomTransferSyntaxes.json Wed May 06 08:40:48 2020 +0200 +++ b/Resources/DicomTransferSyntaxes.json Wed May 20 16:42:44 2020 +0200 @@ -274,7 +274,7 @@ { "UID" : "1.2.840.10008.1.2.4.100", - "Name" : "MPEG2 Main Profile at Main Level", + "Name" : "MPEG2 Main Profile / Main Level", "Value" : "MPEG2MainProfileAtMainLevel", "Retired" : false, "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel" @@ -282,7 +282,7 @@ { "UID" : "1.2.840.10008.1.2.4.101", - "Name" : "MPEG2 Main Profile at High Level", + "Name" : "MPEG2 Main Profile / High Level", "Value" : "MPEG2MainProfileAtHighLevel", "Retired" : false, "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel" @@ -290,7 +290,7 @@ { "UID" : "1.2.840.10008.1.2.4.102", - "Name" : "MPEG4 High Profile / Level 4.1", + "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.1", "Value" : "MPEG4HighProfileLevel4_1", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_1", @@ -299,7 +299,7 @@ { "UID" : "1.2.840.10008.1.2.4.103", - "Name" : "MPEG4 BD-compatible High Profile / Level 4.1", + "Name" : "MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1", "Value" : "MPEG4BDcompatibleHighProfileLevel4_1", "Retired" : false, "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1", @@ -308,7 +308,7 @@ { "UID" : "1.2.840.10008.1.2.4.104", - "Name" : "MPEG4 High Profile / Level 4.2 For 2D Video", + "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video", "Value" : "MPEG4HighProfileLevel4_2_For2DVideo", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo", @@ -317,7 +317,7 @@ { "UID" : "1.2.840.10008.1.2.4.105", - "Name" : "MPEG4 High Profile / Level 4.2 For 3D Video", + "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video", "Value" : "MPEG4HighProfileLevel4_2_For3DVideo", "Retired" : false, "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo", @@ -326,7 +326,7 @@ { "UID" : "1.2.840.10008.1.2.4.106", - "Name" : "1.2.840.10008.1.2.4.106", + "Name" : "MPEG4 AVC/H.264 Stereo High Profile / Level 4.2", "Value" : "MPEG4StereoHighProfileLevel4_2", "Retired" : false, "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2", diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/CallSystemCommand.cpp --- a/Resources/Graveyard/OldScheduler/CallSystemCommand.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "CallSystemCommand.h" - -#include "../../Core/Logging.h" -#include "../../Core/Toolbox.h" -#include "../../Core/TemporaryFile.h" - -namespace Orthanc -{ - CallSystemCommand::CallSystemCommand(ServerContext& context, - const std::string& command, - const std::vector& arguments) : - context_(context), - command_(command), - arguments_(arguments) - { - } - - bool CallSystemCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Calling system command " << command_ << " on instance " << *it; - - try - { - std::string dicom; - context_.ReadDicom(dicom, *it); - - TemporaryFile tmp; - tmp.Write(dicom); - - std::vector args = arguments_; - args.push_back(tmp.GetPath()); - - SystemToolbox::ExecuteSystemCommand(command_, args); - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to call system command " << command_ - << " on instance " << *it << " in a Lua script: " << e.What(); - } - } - - return true; - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/CallSystemCommand.h --- a/Resources/Graveyard/OldScheduler/CallSystemCommand.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class CallSystemCommand : public IServerCommand - { - private: - ServerContext& context_; - std::string command_; - std::vector arguments_; - - public: - CallSystemCommand(ServerContext& context, - const std::string& command, - const std::vector& arguments); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/DeleteInstanceCommand.cpp --- a/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "DeleteInstanceCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - bool DeleteInstanceCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Deleting instance " << *it; - - try - { - Json::Value tmp; - context_.DeleteResource(tmp, *it, ResourceType_Instance); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to delete instance " << *it << ": " << e.What(); - } - } - - return true; - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/DeleteInstanceCommand.h --- a/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class DeleteInstanceCommand : public IServerCommand - { - private: - ServerContext& context_; - - public: - DeleteInstanceCommand(ServerContext& context) : context_(context) - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/IServerCommand.h --- a/Resources/Graveyard/OldScheduler/IServerCommand.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include -#include -#include - -namespace Orthanc -{ - class IServerCommand : public boost::noncopyable - { - public: - typedef std::list ListOfStrings; - - virtual ~IServerCommand() - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) = 0; - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ModifyInstanceCommand.cpp --- a/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,124 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ModifyInstanceCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context, - RequestOrigin origin, - DicomModification* modification) : - context_(context), - origin_(origin), - modification_(modification) - { - modification_->SetAllowManualIdentifiers(true); - - if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID)) - { - modification_->SetLevel(ResourceType_Patient); - } - else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID)) - { - modification_->SetLevel(ResourceType_Study); - } - else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID)) - { - modification_->SetLevel(ResourceType_Series); - } - else - { - modification_->SetLevel(ResourceType_Instance); - } - - if (origin_ != RequestOrigin_Lua) - { - // TODO If issued from HTTP, "remoteIp" and "username" must be provided - throw OrthancException(ErrorCode_NotImplemented); - } - } - - - ModifyInstanceCommand::~ModifyInstanceCommand() - { - if (modification_) - { - delete modification_; - } - } - - - bool ModifyInstanceCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Modifying resource " << *it; - - try - { - std::auto_ptr modified; - - { - ServerContext::DicomCacheLocker lock(context_, *it); - modified.reset(lock.GetDicom().Clone(true)); - } - - modification_->Apply(*modified); - - DicomInstanceToStore toStore; - assert(origin_ == RequestOrigin_Lua); - toStore.SetLuaOrigin(); - toStore.SetParsedDicomFile(*modified); - // TODO other metadata - toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it); - - std::string modifiedId; - context_.Store(modifiedId, toStore); - - // Only chain with other commands if this command succeeds - outputs.push_back(modifiedId); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to modify instance " << *it << ": " << e.What(); - } - } - - return true; - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ModifyInstanceCommand.h --- a/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" -#include "../../Core/DicomParsing/DicomModification.h" - -namespace Orthanc -{ - class ModifyInstanceCommand : public IServerCommand - { - private: - ServerContext& context_; - RequestOrigin origin_; - DicomModification* modification_; - - public: - ModifyInstanceCommand(ServerContext& context, - RequestOrigin origin, - DicomModification* modification); // takes the ownership - - virtual ~ModifyInstanceCommand(); - - const DicomModification& GetModification() const - { - return *modification_; - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.cpp --- a/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,188 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeaders.h" -#include "ReusableDicomUserConnection.h" - -#include "../Logging.h" -#include "../OrthancException.h" - -namespace Orthanc -{ - static boost::posix_time::ptime Now() - { - return boost::posix_time::microsec_clock::local_time(); - } - - void ReusableDicomUserConnection::Open(const std::string& localAet, - const RemoteModalityParameters& remote) - { - if (connection_ != NULL && - connection_->GetLocalApplicationEntityTitle() == localAet && - connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() && - connection_->GetRemoteHost() == remote.GetHost() && - connection_->GetRemotePort() == remote.GetPort() && - connection_->GetRemoteManufacturer() == remote.GetManufacturer()) - { - // The current connection can be reused - LOG(INFO) << "Reusing the previous SCU connection"; - return; - } - - Close(); - - connection_ = new DicomUserConnection(); - connection_->SetLocalApplicationEntityTitle(localAet); - connection_->SetRemoteModality(remote); - connection_->Open(); - } - - void ReusableDicomUserConnection::Close() - { - if (connection_ != NULL) - { - delete connection_; - connection_ = NULL; - } - } - - void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that) - { - for (;;) - { - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); - if (!that->continue_) - { - //LOG(INFO) << "Finishing the thread watching the global SCU connection"; - return; - } - - { - boost::mutex::scoped_lock lock(that->mutex_); - if (that->connection_ != NULL && - Now() >= that->lastUse_ + that->timeBeforeClose_) - { - LOG(INFO) << "Closing the global SCU connection after timeout"; - that->Close(); - } - } - } - } - - - ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that, - const std::string& localAet, - const RemoteModalityParameters& remote) : - ::Orthanc::Locker(that) - { - that.Open(localAet, remote); - connection_ = that.connection_; - } - - - DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection() - { - if (connection_ == NULL) - { - throw OrthancException(ErrorCode_InternalError); - } - - return *connection_; - } - - ReusableDicomUserConnection::ReusableDicomUserConnection() : - connection_(NULL), - timeBeforeClose_(boost::posix_time::seconds(5)) // By default, close connection after 5 seconds - { - lastUse_ = Now(); - continue_ = true; - closeThread_ = boost::thread(CloseThread, this); - } - - ReusableDicomUserConnection::~ReusableDicomUserConnection() - { - if (continue_) - { - LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!"; - Finalize(); - } - } - - void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms) - { - boost::mutex::scoped_lock lock(mutex_); - - if (ms == 0) - { - ms = 1; - } - - timeBeforeClose_ = boost::posix_time::milliseconds(ms); - } - - void ReusableDicomUserConnection::Lock() - { - mutex_.lock(); - } - - void ReusableDicomUserConnection::Unlock() - { - if (connection_ != NULL && - connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp) - { - // "storescp" from DCMTK has problems when reusing a - // connection. Always close. - Close(); - } - - lastUse_ = Now(); - mutex_.unlock(); - } - - - void ReusableDicomUserConnection::Finalize() - { - if (continue_) - { - continue_ = false; - - if (closeThread_.joinable()) - { - closeThread_.join(); - } - - Close(); - } - } -} - diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.h --- a/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "DicomUserConnection.h" -#include "../../Core/MultiThreading/Locker.h" - -#include -#include - -namespace Orthanc -{ - class ReusableDicomUserConnection : public ILockable - { - private: - boost::mutex mutex_; - DicomUserConnection* connection_; - bool continue_; - boost::posix_time::time_duration timeBeforeClose_; - boost::posix_time::ptime lastUse_; - boost::thread closeThread_; - - void Open(const std::string& localAet, - const RemoteModalityParameters& remote); - - void Close(); - - static void CloseThread(ReusableDicomUserConnection* that); - - protected: - virtual void Lock(); - - virtual void Unlock(); - - public: - class Locker : public ::Orthanc::Locker - { - private: - DicomUserConnection* connection_; - - public: - Locker(ReusableDicomUserConnection& that, - const std::string& localAet, - const RemoteModalityParameters& remote); - - DicomUserConnection& GetConnection(); - }; - - ReusableDicomUserConnection(); - - virtual ~ReusableDicomUserConnection(); - - void SetMillisecondsBeforeClose(uint64_t ms); - - void Finalize(); - }; -} - diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ServerCommandInstance.cpp --- a/Resources/Graveyard/OldScheduler/ServerCommandInstance.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ServerCommandInstance.h" - -#include "../../Core/OrthancException.h" - -namespace Orthanc -{ - bool ServerCommandInstance::Execute(IListener& listener) - { - ListOfStrings outputs; - - bool success = false; - - try - { - if (command_->Apply(outputs, inputs_)) - { - success = true; - } - } - catch (OrthancException&) - { - } - - if (!success) - { - listener.SignalFailure(jobId_); - return true; - } - - for (std::list::iterator - it = next_.begin(); it != next_.end(); ++it) - { - for (ListOfStrings::const_iterator - output = outputs.begin(); output != outputs.end(); ++output) - { - (*it)->AddInput(*output); - } - } - - listener.SignalSuccess(jobId_); - return true; - } - - - ServerCommandInstance::ServerCommandInstance(IServerCommand *command, - const std::string& jobId) : - command_(command), - jobId_(jobId), - connectedToSink_(false) - { - if (command_ == NULL) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - } - - - ServerCommandInstance::~ServerCommandInstance() - { - if (command_ != NULL) - { - delete command_; - } - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ServerCommandInstance.h --- a/Resources/Graveyard/OldScheduler/ServerCommandInstance.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,105 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "../../Core/IDynamicObject.h" -#include "IServerCommand.h" - -namespace Orthanc -{ - class ServerCommandInstance : public IDynamicObject - { - friend class ServerScheduler; - - public: - class IListener - { - public: - virtual ~IListener() - { - } - - virtual void SignalSuccess(const std::string& jobId) = 0; - - virtual void SignalFailure(const std::string& jobId) = 0; - }; - - private: - typedef IServerCommand::ListOfStrings ListOfStrings; - - IServerCommand *command_; - std::string jobId_; - ListOfStrings inputs_; - std::list next_; - bool connectedToSink_; - - bool Execute(IListener& listener); - - public: - ServerCommandInstance(IServerCommand *command, - const std::string& jobId); - - virtual ~ServerCommandInstance(); - - const std::string& GetJobId() const - { - return jobId_; - } - - void AddInput(const std::string& input) - { - inputs_.push_back(input); - } - - void ConnectOutput(ServerCommandInstance& next) - { - next_.push_back(&next); - } - - void SetConnectedToSink(bool connected = true) - { - connectedToSink_ = connected; - } - - bool IsConnectedToSink() const - { - return connectedToSink_; - } - - const std::list& GetNextCommands() const - { - return next_; - } - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ServerJob.cpp --- a/Resources/Graveyard/OldScheduler/ServerJob.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,147 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ServerJob.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/Toolbox.h" - -namespace Orthanc -{ - void ServerJob::CheckOrdering() - { - std::map index; - - unsigned int count = 0; - for (std::list::const_iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - index[*it] = count++; - } - - for (std::list::const_iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - const std::list& nextCommands = (*it)->GetNextCommands(); - - for (std::list::const_iterator - next = nextCommands.begin(); next != nextCommands.end(); ++next) - { - if (index.find(*next) == index.end() || - index[*next] <= index[*it]) - { - // You must reorder your calls to "ServerJob::AddCommand" - throw OrthancException(ErrorCode_BadJobOrdering); - } - } - } - } - - - size_t ServerJob::Submit(SharedMessageQueue& target, - ServerCommandInstance::IListener& listener) - { - if (submitted_) - { - // This job has already been submitted - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - CheckOrdering(); - - size_t size = filters_.size(); - - for (std::list::iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - target.Enqueue(*it); - } - - filters_.clear(); - submitted_ = true; - - return size; - } - - - ServerJob::ServerJob() : - jobId_(Toolbox::GenerateUuid()), - submitted_(false), - description_("no description") - { - } - - - ServerJob::~ServerJob() - { - for (std::list::iterator - it = filters_.begin(); it != filters_.end(); ++it) - { - delete *it; - } - - for (std::list::iterator - it = payloads_.begin(); it != payloads_.end(); ++it) - { - delete *it; - } - } - - - ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter) - { - if (submitted_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - filters_.push_back(new ServerCommandInstance(filter, jobId_)); - - return *filters_.back(); - } - - - IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload) - { - if (submitted_) - { - throw OrthancException(ErrorCode_BadSequenceOfCalls); - } - - payloads_.push_back(payload); - - return *filters_.back(); - } - -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ServerJob.h --- a/Resources/Graveyard/OldScheduler/ServerJob.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ServerCommandInstance.h" -#include "../../Core/MultiThreading/SharedMessageQueue.h" - -namespace Orthanc -{ - class ServerJob - { - friend class ServerScheduler; - - private: - std::list filters_; - std::list payloads_; - std::string jobId_; - bool submitted_; - std::string description_; - - void CheckOrdering(); - - size_t Submit(SharedMessageQueue& target, - ServerCommandInstance::IListener& listener); - - public: - ServerJob(); - - ~ServerJob(); - - const std::string& GetId() const - { - return jobId_; - } - - void SetDescription(const std::string& description) - { - description_ = description; - } - - const std::string& GetDescription() const - { - return description_; - } - - ServerCommandInstance& AddCommand(IServerCommand* filter); - - // Take the ownership of a payload to a job. This payload will be - // automatically freed when the job succeeds or fails. - IDynamicObject& AddPayload(IDynamicObject* payload); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ServerScheduler.cpp --- a/Resources/Graveyard/OldScheduler/ServerScheduler.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,359 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "ServerScheduler.h" - -#include "../../Core/OrthancException.h" -#include "../../Core/Logging.h" - -namespace Orthanc -{ - namespace - { - // Anonymous namespace to avoid clashes between compilation modules - class Sink : public IServerCommand - { - private: - ListOfStrings& target_; - - public: - explicit Sink(ListOfStrings& target) : target_(target) - { - } - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - target_.push_back(*it); - } - - return true; - } - }; - } - - - ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId) - { - Jobs::iterator info = jobs_.find(jobId); - - if (info == jobs_.end()) - { - throw OrthancException(ErrorCode_InternalError); - } - - return info->second; - } - - - void ServerScheduler::SignalSuccess(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - JobInfo& info = GetJobInfo(jobId); - info.success_++; - - assert(info.failures_ == 0); - - if (info.success_ >= info.size_) - { - if (info.watched_) - { - watchedJobStatus_[jobId] = JobStatus_Success; - watchedJobFinished_.notify_all(); - } - - LOG(INFO) << "Job successfully finished (" << info.description_ << ")"; - jobs_.erase(jobId); - - availableJob_.Release(); - } - } - - - void ServerScheduler::SignalFailure(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - JobInfo& info = GetJobInfo(jobId); - info.failures_++; - - if (info.success_ + info.failures_ >= info.size_) - { - if (info.watched_) - { - watchedJobStatus_[jobId] = JobStatus_Failure; - watchedJobFinished_.notify_all(); - } - - LOG(ERROR) << "Job has failed (" << info.description_ << ")"; - jobs_.erase(jobId); - - availableJob_.Release(); - } - } - - - void ServerScheduler::Worker(ServerScheduler* that) - { - static const int32_t TIMEOUT = 100; - - LOG(WARNING) << "The server scheduler has started"; - - while (!that->finish_) - { - std::auto_ptr object(that->queue_.Dequeue(TIMEOUT)); - if (object.get() != NULL) - { - ServerCommandInstance& filter = dynamic_cast(*object); - - // Skip the execution of this filter if its parent job has - // previously failed. - bool jobHasFailed; - { - boost::mutex::scoped_lock lock(that->mutex_); - JobInfo& info = that->GetJobInfo(filter.GetJobId()); - jobHasFailed = (info.failures_ > 0 || info.cancel_); - } - - if (jobHasFailed) - { - that->SignalFailure(filter.GetJobId()); - } - else - { - filter.Execute(*that); - } - } - } - } - - - void ServerScheduler::SubmitInternal(ServerJob& job, - bool watched) - { - availableJob_.Acquire(); - - boost::mutex::scoped_lock lock(mutex_); - - JobInfo info; - info.size_ = job.Submit(queue_, *this); - info.cancel_ = false; - info.success_ = 0; - info.failures_ = 0; - info.description_ = job.GetDescription(); - info.watched_ = watched; - - assert(info.size_ > 0); - - if (watched) - { - watchedJobStatus_[job.GetId()] = JobStatus_Running; - } - - jobs_[job.GetId()] = info; - - LOG(INFO) << "New job submitted (" << job.description_ << ")"; - } - - - ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs) - { - if (maxJobs == 0) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - finish_ = false; - worker_ = boost::thread(Worker, this); - } - - - ServerScheduler::~ServerScheduler() - { - if (!finish_) - { - LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!"; - Stop(); - } - } - - - void ServerScheduler::Stop() - { - if (!finish_) - { - finish_ = true; - - if (worker_.joinable()) - { - worker_.join(); - } - } - } - - - void ServerScheduler::Submit(ServerJob& job) - { - if (job.filters_.empty()) - { - return; - } - - SubmitInternal(job, false); - } - - - bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs, - ServerJob& job) - { - std::string jobId = job.GetId(); - - outputs.clear(); - - if (job.filters_.empty()) - { - return true; - } - - // Add a sink filter to collect all the results of the filters - // that have no next filter. - ServerCommandInstance& sink = job.AddCommand(new Sink(outputs)); - - for (std::list::iterator - it = job.filters_.begin(); it != job.filters_.end(); ++it) - { - if ((*it) != &sink && - (*it)->IsConnectedToSink()) - { - (*it)->ConnectOutput(sink); - } - } - - // Submit the job - SubmitInternal(job, true); - - // Wait for the job to complete (either success or failure) - JobStatus status; - - { - boost::mutex::scoped_lock lock(mutex_); - - assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end()); - - while (watchedJobStatus_[jobId] == JobStatus_Running) - { - watchedJobFinished_.wait(lock); - } - - status = watchedJobStatus_[jobId]; - watchedJobStatus_.erase(jobId); - } - - return (status == JobStatus_Success); - } - - - bool ServerScheduler::SubmitAndWait(ServerJob& job) - { - ListOfStrings ignoredSink; - return SubmitAndWait(ignoredSink, job); - } - - - bool ServerScheduler::IsRunning(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - return jobs_.find(jobId) != jobs_.end(); - } - - - void ServerScheduler::Cancel(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - Jobs::iterator job = jobs_.find(jobId); - - if (job != jobs_.end()) - { - job->second.cancel_ = true; - LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")"; - } - } - - - float ServerScheduler::GetProgress(const std::string& jobId) - { - boost::mutex::scoped_lock lock(mutex_); - - Jobs::iterator job = jobs_.find(jobId); - - if (job == jobs_.end() || - job->second.size_ == 0 /* should never happen */) - { - // This job is not running - return 1; - } - - if (job->second.failures_ != 0) - { - return 1; - } - - if (job->second.size_ == 1) - { - return static_cast(job->second.success_); - } - - return (static_cast(job->second.success_) / - static_cast(job->second.size_ - 1)); - } - - - void ServerScheduler::GetListOfJobs(ListOfStrings& jobs) - { - boost::mutex::scoped_lock lock(mutex_); - - jobs.clear(); - - for (Jobs::const_iterator - it = jobs_.begin(); it != jobs_.end(); ++it) - { - jobs.push_back(it->first); - } - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/ServerScheduler.h --- a/Resources/Graveyard/OldScheduler/ServerScheduler.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,123 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "ServerJob.h" - -#include "../../Core/MultiThreading/Semaphore.h" - -namespace Orthanc -{ - class ServerScheduler : public ServerCommandInstance::IListener - { - private: - struct JobInfo - { - bool watched_; - bool cancel_; - size_t size_; - size_t success_; - size_t failures_; - std::string description_; - }; - - enum JobStatus - { - JobStatus_Running = 1, - JobStatus_Success = 2, - JobStatus_Failure = 3 - }; - - typedef IServerCommand::ListOfStrings ListOfStrings; - typedef std::map Jobs; - - boost::mutex mutex_; - boost::condition_variable watchedJobFinished_; - Jobs jobs_; - SharedMessageQueue queue_; - bool finish_; - boost::thread worker_; - std::map watchedJobStatus_; - Semaphore availableJob_; - - JobInfo& GetJobInfo(const std::string& jobId); - - virtual void SignalSuccess(const std::string& jobId); - - virtual void SignalFailure(const std::string& jobId); - - static void Worker(ServerScheduler* that); - - void SubmitInternal(ServerJob& job, - bool watched); - - public: - explicit ServerScheduler(unsigned int maxjobs); - - ~ServerScheduler(); - - void Stop(); - - void Submit(ServerJob& job); - - bool SubmitAndWait(ListOfStrings& outputs, - ServerJob& job); - - bool SubmitAndWait(ServerJob& job); - - bool IsRunning(const std::string& jobId); - - void Cancel(const std::string& jobId); - - // Returns a number between 0 and 1 - float GetProgress(const std::string& jobId); - - bool IsRunning(const ServerJob& job) - { - return IsRunning(job.GetId()); - } - - void Cancel(const ServerJob& job) - { - Cancel(job.GetId()); - } - - float GetProgress(const ServerJob& job) - { - return GetProgress(job.GetId()); - } - - void GetListOfJobs(ListOfStrings& jobs); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/StorePeerCommand.cpp --- a/Resources/Graveyard/OldScheduler/StorePeerCommand.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "StorePeerCommand.h" - -#include "../../Core/Logging.h" -#include "../../Core/HttpClient.h" - -namespace Orthanc -{ - StorePeerCommand::StorePeerCommand(ServerContext& context, - const WebServiceParameters& peer, - bool ignoreExceptions) : - context_(context), - peer_(peer), - ignoreExceptions_(ignoreExceptions) - { - } - - bool StorePeerCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - // Configure the HTTP client - HttpClient client(peer_, "instances"); - client.SetMethod(HttpMethod_Post); - - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to peer \"" - << peer_.GetUrl() << "\""; - - try - { - context_.ReadDicom(client.GetBody(), *it); - - std::string answer; - if (!client.Apply(answer)) - { - LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\""; - throw OrthancException(ErrorCode_NetworkProtocol); - } - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - LOG(ERROR) << "Unable to forward to an Orthanc peer in (instance " - << *it << ", peer " << peer_.GetUrl() << "): " << e.What(); - - if (!ignoreExceptions_) - { - throw; - } - } - } - - return true; - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/StorePeerCommand.h --- a/Resources/Graveyard/OldScheduler/StorePeerCommand.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" -#include "../OrthancInitialization.h" - -namespace Orthanc -{ - class StorePeerCommand : public IServerCommand - { - private: - ServerContext& context_; - WebServiceParameters peer_; - bool ignoreExceptions_; - - public: - StorePeerCommand(ServerContext& context, - const WebServiceParameters& peer, - bool ignoreExceptions); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/StoreScuCommand.cpp --- a/Resources/Graveyard/OldScheduler/StoreScuCommand.cpp Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#include "../PrecompiledHeadersServer.h" -#include "StoreScuCommand.h" - -#include "../../Core/Logging.h" - -namespace Orthanc -{ - StoreScuCommand::StoreScuCommand(ServerContext& context, - const std::string& localAet, - const RemoteModalityParameters& modality, - bool ignoreExceptions) : - context_(context), - modality_(modality), - ignoreExceptions_(ignoreExceptions), - localAet_(localAet), - moveOriginatorID_(0) - { - } - - - void StoreScuCommand::SetMoveOriginator(const std::string& aet, - uint16_t id) - { - moveOriginatorAET_ = aet; - moveOriginatorID_ = id; - } - - - bool StoreScuCommand::Apply(ListOfStrings& outputs, - const ListOfStrings& inputs) - { - ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); - - for (ListOfStrings::const_iterator - it = inputs.begin(); it != inputs.end(); ++it) - { - LOG(INFO) << "Sending resource " << *it << " to modality \"" - << modality_.GetApplicationEntityTitle() << "\""; - - try - { - std::string dicom; - context_.ReadDicom(dicom, *it); - - locker.GetConnection().Store(dicom, moveOriginatorAET_, moveOriginatorID_); - - // Only chain with other commands if this command succeeds - outputs.push_back(*it); - } - catch (OrthancException& e) - { - // Ignore transmission errors (e.g. if the remote modality is - // powered off) - LOG(ERROR) << "Unable to forward to a modality in (instance " - << *it << "): " << e.What(); - - if (!ignoreExceptions_) - { - throw; - } - } - } - - return true; - } -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/OldScheduler/StoreScuCommand.h --- a/Resources/Graveyard/OldScheduler/StoreScuCommand.h Wed May 06 08:40:48 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2020 Osimis S.A., Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * In addition, as a special exception, the copyright holders of this - * program give permission to link the code of its release with the - * OpenSSL project's "OpenSSL" library (or with modified versions of it - * that use the same license as the "OpenSSL" library), and distribute - * the linked executables. You must obey the GNU General Public License - * in all respects for all of the code used other than "OpenSSL". If you - * modify file(s) with this exception, you may extend this exception to - * your version of the file(s), but you are not obligated to do so. If - * you do not wish to do so, delete this exception statement from your - * version. If you delete this exception statement from all source files - * in the program, then also delete it here. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - **/ - - -#pragma once - -#include "IServerCommand.h" -#include "../ServerContext.h" - -namespace Orthanc -{ - class StoreScuCommand : public IServerCommand - { - private: - ServerContext& context_; - RemoteModalityParameters modality_; - bool ignoreExceptions_; - std::string localAet_; - std::string moveOriginatorAET_; - uint16_t moveOriginatorID_; - - public: - StoreScuCommand(ServerContext& context, - const std::string& localAet, - const RemoteModalityParameters& modality, - bool ignoreExceptions); - - void SetMoveOriginator(const std::string& aet, - uint16_t id); - - virtual bool Apply(ListOfStrings& outputs, - const ListOfStrings& inputs); - }; -} diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Graveyard/TestTranscoding.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Graveyard/TestTranscoding.cpp Wed May 20 16:42:44 2020 +0200 @@ -0,0 +1,967 @@ + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmFileFormat& dicom, + DicomTransferSyntax syntax) + { + E_TransferSyntax xfer; + if (!LookupDcmtkTransferSyntax(xfer, syntax)) + { + return false; + } + else if (!dicom.validateMetaInfo(xfer).good()) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot setup the transfer syntax to write a DICOM instance"); + } + else + { + return SaveToMemoryBufferInternal(buffer, dicom, xfer); + } + } + + + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, + DcmFileFormat& dicom) + { + E_TransferSyntax xfer = dicom.getDataset()->getCurrentXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot write a DICOM instance with unknown transfer syntax"); + } + else if (!dicom.validateMetaInfo(xfer).good()) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot setup the transfer syntax to write a DICOM instance"); + } + else + { + return SaveToMemoryBufferInternal(buffer, dicom, xfer); + } + } + + + + + +#include +#include +#include + +#include "../Core/DicomParsing/Internals/DicomFrameIndex.h" + +namespace Orthanc +{ + class IParsedDicomImage : public boost::noncopyable + { + public: + virtual ~IParsedDicomImage() + { + } + + virtual DicomTransferSyntax GetTransferSyntax() = 0; + + virtual std::string GetSopClassUid() = 0; + + virtual std::string GetSopInstanceUid() = 0; + + virtual unsigned int GetFramesCount() = 0; + + // Can return NULL, for compressed transfer syntaxes + virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) = 0; + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) = 0; + + virtual void WriteToMemoryBuffer(std::string& target) = 0; + }; + + + class IDicomImageReader : public boost::noncopyable + { + public: + virtual ~IDicomImageReader() + { + } + + virtual IParsedDicomImage* Read(const void* data, + size_t size) = 0; + + virtual IParsedDicomImage* Transcode(const void* data, + size_t size, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) = 0; + }; + + + class DcmtkImageReader : public IDicomImageReader + { + private: + class Image : public IParsedDicomImage + { + private: + std::unique_ptr dicom_; + std::unique_ptr index_; + DicomTransferSyntax transferSyntax_; + std::string sopClassUid_; + std::string sopInstanceUid_; + + static std::string GetStringTag(DcmDataset& dataset, + const DcmTagKey& tag) + { + const char* value = NULL; + + if (!dataset.findAndGetString(tag, value).good() || + value == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing SOP class/instance UID in DICOM instance"); + } + else + { + return std::string(value); + } + } + + public: + Image(DcmFileFormat* dicom, + DicomTransferSyntax syntax) : + dicom_(dicom), + transferSyntax_(syntax) + { + if (dicom == NULL || + dicom_->getDataset() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DcmDataset& dataset = *dicom_->getDataset(); + index_.reset(new DicomFrameIndex(dataset)); + + sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID); + sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID); + } + + virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE + { + return transferSyntax_; + } + + virtual std::string GetSopClassUid() ORTHANC_OVERRIDE + { + return sopClassUid_; + } + + virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE + { + return sopInstanceUid_; + } + + virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE + { + return index_->GetFramesCount(); + } + + virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE + { + assert(dicom_.get() != NULL); + if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, transferSyntax_)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot write the DICOM instance to a memory buffer"); + } + } + + virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) ORTHANC_OVERRIDE + { + assert(dicom_.get() != NULL && + dicom_->getDataset() != NULL); + return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); + } + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) ORTHANC_OVERRIDE + { + assert(index_.get() != NULL); + index_->GetRawFrame(target, frame); + } + }; + + unsigned int lossyQuality_; + + static DicomTransferSyntax DetectTransferSyntax(DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DcmDataset& dataset = *dicom.getDataset(); + + E_TransferSyntax xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + dataset.updateOriginalXfer(); + xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax of the DICOM instance"); + } + } + + DicomTransferSyntax syntax; + if (FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, xfer)) + { + return syntax; + } + else + { + throw OrthancException( + ErrorCode_BadFileFormat, + "Unsupported transfer syntax: " + boost::lexical_cast(xfer)); + } + } + + + static uint16_t GetBitsStored(DcmFileFormat& dicom) + { + if (dicom.getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + uint16_t bitsStored; + if (dicom.getDataset()->findAndGetUint16(DCM_BitsStored, bitsStored).good()) + { + return bitsStored; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing \"Bits Stored\" tag in DICOM instance"); + } + } + + + public: + DcmtkImageReader() : + lossyQuality_(90) + { + } + + void SetLossyQuality(unsigned int quality) + { + if (quality <= 0 || + quality > 100) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + lossyQuality_ = quality; + } + } + + unsigned int GetLossyQuality() const + { + return lossyQuality_; + } + + virtual IParsedDicomImage* Read(const void* data, + size_t size) + { + std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size)); + if (dicom.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + DicomTransferSyntax transferSyntax = DetectTransferSyntax(*dicom); + + return new Image(dicom.release(), transferSyntax); + } + + virtual IParsedDicomImage* Transcode(const void* data, + size_t size, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) + { + std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size)); + if (dicom.get() == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + + const uint16_t bitsStored = GetBitsStored(*dicom); + + if (syntax == DetectTransferSyntax(*dicom)) + { + // No transcoding is needed + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_LittleEndianImplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianImplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_LittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianExplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_BigEndianExplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_BigEndianExplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + + if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL)) + { + return new Image(dicom.release(), syntax); + } + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess1 && + allowNewSopInstanceUid && + bitsStored == 8) + { + DJ_RPLossy rpLossy(lossyQuality_); + + if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess1, &rpLossy)) + { + return new Image(dicom.release(), syntax); + } + } +#endif + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess2_4 && + allowNewSopInstanceUid && + bitsStored <= 12) + { + DJ_RPLossy rpLossy(lossyQuality_); + if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess2_4, &rpLossy)) + { + return new Image(dicom.release(), syntax); + } + } +#endif + + //LOG(INFO) << "Unable to transcode DICOM image using the built-in reader"; + return NULL; + } + }; + + + + class IDicomTranscoder1 : public boost::noncopyable + { + public: + virtual ~IDicomTranscoder1() + { + } + + virtual DcmFileFormat& GetDicom() = 0; + + virtual DicomTransferSyntax GetTransferSyntax() = 0; + + virtual std::string GetSopClassUid() = 0; + + virtual std::string GetSopInstanceUid() = 0; + + virtual unsigned int GetFramesCount() = 0; + + virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0; + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) = 0; + + // NB: Transcoding can change the value of "GetSopInstanceUid()" + // and "GetTransferSyntax()" if lossy compression is applied + virtual bool Transcode(std::string& target, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) = 0; + + virtual void WriteToMemoryBuffer(std::string& target) = 0; + }; + + + class DcmtkTranscoder2 : public IDicomTranscoder1 + { + private: + std::unique_ptr dicom_; + std::unique_ptr index_; + DicomTransferSyntax transferSyntax_; + std::string sopClassUid_; + std::string sopInstanceUid_; + uint16_t bitsStored_; + unsigned int lossyQuality_; + + static std::string GetStringTag(DcmDataset& dataset, + const DcmTagKey& tag) + { + const char* value = NULL; + + if (!dataset.findAndGetString(tag, value).good() || + value == NULL) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing SOP class/instance UID in DICOM instance"); + } + else + { + return std::string(value); + } + } + + void Setup(DcmFileFormat* dicom) + { + lossyQuality_ = 90; + + dicom_.reset(dicom); + + if (dicom == NULL || + dicom_->getDataset() == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + + DcmDataset& dataset = *dicom_->getDataset(); + index_.reset(new DicomFrameIndex(dataset)); + + E_TransferSyntax xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + dataset.updateOriginalXfer(); + xfer = dataset.getCurrentXfer(); + if (xfer == EXS_Unknown) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Cannot determine the transfer syntax of the DICOM instance"); + } + } + + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer)) + { + throw OrthancException( + ErrorCode_BadFileFormat, + "Unsupported transfer syntax: " + boost::lexical_cast(xfer)); + } + + if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored_).good()) + { + throw OrthancException(ErrorCode_BadFileFormat, + "Missing \"Bits Stored\" tag in DICOM instance"); + } + + sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID); + sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID); + } + + public: + DcmtkTranscoder2(DcmFileFormat* dicom) // Takes ownership + { + Setup(dicom); + } + + DcmtkTranscoder2(const void* dicom, + size_t size) + { + Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size)); + } + + void SetLossyQuality(unsigned int quality) + { + if (quality <= 0 || + quality > 100) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + lossyQuality_ = quality; + } + } + + unsigned int GetLossyQuality() const + { + return lossyQuality_; + } + + unsigned int GetBitsStored() const + { + return bitsStored_; + } + + virtual DcmFileFormat& GetDicom() + { + assert(dicom_ != NULL); + return *dicom_; + } + + virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE + { + return transferSyntax_; + } + + virtual std::string GetSopClassUid() ORTHANC_OVERRIDE + { + return sopClassUid_; + } + + virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE + { + return sopInstanceUid_; + } + + virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE + { + return index_->GetFramesCount(); + } + + virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE + { + if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_)) + { + throw OrthancException(ErrorCode_InternalError, + "Cannot write the DICOM instance to a memory buffer"); + } + } + + virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE + { + assert(dicom_->getDataset() != NULL); + return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); + } + + virtual void GetCompressedFrame(std::string& target, + unsigned int frame) ORTHANC_OVERRIDE + { + index_->GetRawFrame(target, frame); + } + + virtual bool Transcode(std::string& target, + DicomTransferSyntax syntax, + bool allowNewSopInstanceUid) ORTHANC_OVERRIDE + { + assert(dicom_ != NULL && + dicom_->getDataset() != NULL); + + if (syntax == GetTransferSyntax()) + { + printf("NO TRANSCODING\n"); + + // No change in the transfer syntax => simply serialize the current dataset + WriteToMemoryBuffer(target); + return true; + } + + printf(">> %d\n", bitsStored_); + + if (syntax == DicomTransferSyntax_LittleEndianImplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_LittleEndianImplicit; + return true; + } + + if (syntax == DicomTransferSyntax_LittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_LittleEndianExplicit; + return true; + } + + if (syntax == DicomTransferSyntax_BigEndianExplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_BigEndianExplicit; + return true; + } + + if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit && + FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_DeflatedLittleEndianExplicit; + return true; + } + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess1 && + allowNewSopInstanceUid && + GetBitsStored() == 8) + { + DJ_RPLossy rpLossy(lossyQuality_); + + if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_JPEGProcess1; + sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID); + return true; + } + } +#endif + +#if ORTHANC_ENABLE_JPEG == 1 + if (syntax == DicomTransferSyntax_JPEGProcess2_4 && + allowNewSopInstanceUid && + GetBitsStored() <= 12) + { + DJ_RPLossy rpLossy(lossyQuality_); + if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) && + FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax)) + { + transferSyntax_ = DicomTransferSyntax_JPEGProcess2_4; + sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID); + return true; + } + } +#endif + + return false; + } + }; +} + + + + +#include + + +static void TestFile(const std::string& path) +{ + static unsigned int count = 0; + count++; + + + printf("** %s\n", path.c_str()); + + std::string s; + SystemToolbox::ReadFile(s, path); + + Orthanc::DcmtkTranscoder2 transcoder(s.c_str(), s.size()); + + /*if (transcoder.GetBitsStored() != 8) // TODO + return; */ + + { + char buf[1024]; + sprintf(buf, "/tmp/source-%06d.dcm", count); + printf(">> %s\n", buf); + Orthanc::SystemToolbox::WriteFile(s, buf); + } + + printf("[%s] [%s] [%s] %d %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()), + transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(), + transcoder.GetFramesCount(), transcoder.GetTransferSyntax()); + + for (size_t i = 0; i < transcoder.GetFramesCount(); i++) + { + std::string f; + transcoder.GetCompressedFrame(f, i); + + if (i == 0) + { + char buf[1024]; + sprintf(buf, "/tmp/frame-%06d.raw", count); + printf(">> %s\n", buf); + Orthanc::SystemToolbox::WriteFile(f, buf); + } + } + + { + std::string t; + transcoder.WriteToMemoryBuffer(t); + + Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size()); + printf(">> %d %d ; %lu bytes\n", transcoder.GetTransferSyntax(), transcoder2.GetTransferSyntax(), t.size()); + } + + { + std::string a = transcoder.GetSopInstanceUid(); + DicomTransferSyntax b = transcoder.GetTransferSyntax(); + + DicomTransferSyntax syntax = DicomTransferSyntax_JPEGProcess2_4; + //DicomTransferSyntax syntax = DicomTransferSyntax_LittleEndianExplicit; + + std::string t; + bool ok = transcoder.Transcode(t, syntax, true); + printf("Transcoding: %d\n", ok); + + if (ok) + { + printf("[%s] => [%s]\n", a.c_str(), transcoder.GetSopInstanceUid().c_str()); + printf("[%s] => [%s]\n", GetTransferSyntaxUid(b), + GetTransferSyntaxUid(transcoder.GetTransferSyntax())); + + { + char buf[1024]; + sprintf(buf, "/tmp/transcoded-%06d.dcm", count); + printf(">> %s\n", buf); + Orthanc::SystemToolbox::WriteFile(t, buf); + } + + Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size()); + printf(" => transcoded transfer syntax %d ; %lu bytes\n", transcoder2.GetTransferSyntax(), t.size()); + } + } + + printf("\n"); +} + +TEST(Toto, DISABLED_Transcode) +{ + //OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); + + if (1) + { + const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes"; + + for (boost::filesystem::directory_iterator it(PATH); + it != boost::filesystem::directory_iterator(); ++it) + { + if (boost::filesystem::is_regular_file(it->status())) + { + TestFile(it->path().string()); + } + } + } + + if (0) + { + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm"); + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm"); + } + + if (0) + { + TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm"); + } +} + + +TEST(Toto, DISABLED_Transcode2) +{ + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string(GetTransferSyntaxUid(a)) + ".dcm"); + if (Orthanc::SystemToolbox::IsRegularFile(path)) + { + printf("\n======= %s\n", GetTransferSyntaxUid(a)); + + std::string source; + Orthanc::SystemToolbox::ReadFile(source, path); + + DcmtkImageReader reader; + + { + std::unique_ptr image( + reader.Read(source.c_str(), source.size())); + ASSERT_TRUE(image.get() != NULL); + ASSERT_EQ(a, image->GetTransferSyntax()); + + std::string target; + image->WriteToMemoryBuffer(target); + } + + for (int j = 0; j <= DicomTransferSyntax_XML; j++) + { + DicomTransferSyntax b = (DicomTransferSyntax) j; + //if (a == b) continue; + + std::unique_ptr image( + reader.Transcode(source.c_str(), source.size(), b, true)); + if (image.get() != NULL) + { + printf("[%s] -> [%s]\n", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b)); + + std::string target; + image->WriteToMemoryBuffer(target); + + char buf[1024]; + sprintf(buf, "/tmp/%s-%s.dcm", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b)); + + SystemToolbox::WriteFile(target, buf); + } + else if (a != DicomTransferSyntax_JPEG2000 && + a != DicomTransferSyntax_JPEG2000LosslessOnly) + { + ASSERT_TRUE(b != DicomTransferSyntax_LittleEndianImplicit && + b != DicomTransferSyntax_LittleEndianExplicit && + b != DicomTransferSyntax_BigEndianExplicit && + b != DicomTransferSyntax_DeflatedLittleEndianExplicit); + } + } + } + } +} + + +#include "../Core/DicomNetworking/DicomAssociation.h" +#include "../Core/DicomNetworking/DicomControlUserConnection.h" +#include "../Core/DicomNetworking/DicomStoreUserConnection.h" + +TEST(Toto, DISABLED_DicomAssociation) +{ + DicomAssociationParameters params; + params.SetLocalApplicationEntityTitle("ORTHANC"); + params.SetRemoteApplicationEntityTitle("PACS"); + params.SetRemotePort(2001); + +#if 0 + DicomAssociation assoc; + assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass); + assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEGProcess1); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEGProcess2_4); + assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, + DicomTransferSyntax_JPEG2000); + + assoc.Open(params); + + int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage); + printf(">> %d\n", presID); + + std::map pc; + printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage)); + + for (std::map::const_iterator + it = pc.begin(); it != pc.end(); ++it) + { + printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second); + } +#else + { + DicomControlUserConnection assoc(params); + + try + { + printf(">> %d\n", assoc.Echo()); + } + catch (OrthancException&) + { + } + } + + params.SetRemoteApplicationEntityTitle("PACS"); + params.SetRemotePort(2000); + + { + DicomControlUserConnection assoc(params); + printf(">> %d\n", assoc.Echo()); + } + +#endif +} + +static void TestTranscode(DicomStoreUserConnection& scu, + const std::string& sopClassUid, + DicomTransferSyntax transferSyntax) +{ + std::set accepted; + + scu.LookupTranscoding(accepted, sopClassUid, transferSyntax); + if (accepted.empty()) + { + throw OrthancException(ErrorCode_NetworkProtocol, + "The SOP class is not supported by the remote modality"); + } + + { + unsigned int count = 0; + for (std::set::const_iterator + it = accepted.begin(); it != accepted.end(); ++it) + { + LOG(INFO) << "available for transcoding " << (count++) << ": " << sopClassUid + << " / " << GetTransferSyntaxUid(*it); + } + } + + if (accepted.find(transferSyntax) != accepted.end()) + { + printf("**** OK, without transcoding !! [%s]\n", GetTransferSyntaxUid(transferSyntax)); + } + else + { + // Transcoding - only in Orthanc >= 1.7.0 + + const DicomTransferSyntax uncompressed[] = { + DicomTransferSyntax_LittleEndianImplicit, // Default transfer syntax + DicomTransferSyntax_LittleEndianExplicit, + DicomTransferSyntax_BigEndianExplicit + }; + + bool found = false; + for (size_t i = 0; i < 3; i++) + { + if (accepted.find(uncompressed[i]) != accepted.end()) + { + printf("**** TRANSCODING to %s\n", GetTransferSyntaxUid(uncompressed[i])); + found = true; + break; + } + } + + if (!found) + { + printf("**** KO KO KO\n"); + } + } +} + + +TEST(Toto, DISABLED_Store) +{ + DicomAssociationParameters params; + params.SetLocalApplicationEntityTitle("ORTHANC"); + params.SetRemoteApplicationEntityTitle("STORESCP"); + params.SetRemotePort(2000); + + DicomStoreUserConnection assoc(params); + assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess1); + assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4); + //assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); + + //assoc.SetUncompressedSyntaxesProposed(false); // Necessary for transcoding + assoc.SetCommonClassesProposed(false); + assoc.SetRetiredBigEndianProposed(true); + TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit); + TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000); + TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000); +} + + +TEST(Toto, DISABLED_Store2) +{ + DicomAssociationParameters params; + params.SetLocalApplicationEntityTitle("ORTHANC"); + params.SetRemoteApplicationEntityTitle("STORESCP"); + params.SetRemotePort(2000); + + DicomStoreUserConnection assoc(params); + //assoc.SetCommonClassesProposed(false); + assoc.SetRetiredBigEndianProposed(true); + + std::string s; + Orthanc::SystemToolbox::ReadFile(s, "/tmp/i/" + std::string(GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit)) +".dcm"); + + std::string c, i; + assoc.Store(c, i, s.c_str(), s.size()); + printf("[%s] [%s]\n", c.c_str(), i.c_str()); +} + diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/Orthanc.doxygen --- a/Resources/Orthanc.doxygen Wed May 06 08:40:48 2020 +0200 +++ b/Resources/Orthanc.doxygen Wed May 20 16:42:44 2020 +0200 @@ -545,7 +545,7 @@ # this will also influence the order of the classes in the class list. # The default value is: NO. -SORT_BRIEF_DOCS = NO +SORT_BRIEF_DOCS = YES # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the # (brief and detailed) documentation of class members so that constructors and diff -r fe0e4ef52a72 -r 6e14f2da7c7e Resources/OrthancPlugin.doxygen --- a/Resources/OrthancPlugin.doxygen Wed May 06 08:40:48 2020 +0200 +++ b/Resources/OrthancPlugin.doxygen Wed May 20 16:42:44 2020 +0200 @@ -545,7 +545,7 @@ # this will also influence the order of the classes in the class list. # The default value is: NO. -SORT_BRIEF_DOCS = NO +SORT_BRIEF_DOCS = YES # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the # (brief and detailed) documentation of class members so that constructors and diff -r fe0e4ef52a72 -r 6e14f2da7c7e UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Wed May 06 08:40:48 2020 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Wed May 20 16:42:44 2020 +0200 @@ -513,7 +513,8 @@ f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata"); // (**) std::string s; - ASSERT_FALSE(f.LookupTransferSyntax(s)); + ASSERT_TRUE(f.LookupTransferSyntax(s)); + ASSERT_EQ(s, GetTransferSyntaxUid(DicomTransferSyntax_LittleEndianExplicit)); ASSERT_THROW(f.Replace(DICOM_TAG_ACCESSION_NUMBER, std::string("Accession"), false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException); @@ -1924,349 +1925,105 @@ #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1 -#include "../Core/DicomParsing/Internals/DicomFrameIndex.h" - -#include -#include -#include - - -namespace Orthanc -{ - class IDicomTranscoder : public boost::noncopyable - { - public: - virtual ~IDicomTranscoder() - { - } +#include "../Core/DicomNetworking/DicomStoreUserConnection.h" +#include "../Core/DicomParsing/DcmtkTranscoder.h" - virtual DicomTransferSyntax GetTransferSyntax() = 0; - - virtual std::string GetSopClassUid() = 0; - - virtual std::string GetSopInstanceUid() = 0; - - virtual unsigned int GetFramesCount() = 0; - - virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0; - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) = 0; - - virtual IDicomTranscoder* Transcode(std::set syntaxes, - bool allowNewSopInstanceUid) = 0; - }; - +TEST(Toto, DISABLED_Transcode3) +{ + DicomAssociationParameters p; + p.SetRemotePort(2000); - class DcmtkTranscoder : public IDicomTranscoder - { - private: - std::unique_ptr dicom_; - std::unique_ptr index_; - DicomTransferSyntax transferSyntax_; - std::string sopClassUid_; - std::string sopInstanceUid_; - - void Setup(DcmFileFormat* dicom) - { - dicom_.reset(dicom); - - if (dicom == NULL || - dicom_->getDataset() == NULL) - { - throw OrthancException(ErrorCode_NullPointer); - } - - DcmDataset& dataset = *dicom_->getDataset(); - index_.reset(new DicomFrameIndex(dataset)); + DicomStoreUserConnection scu(p); + scu.SetCommonClassesProposed(false); + scu.SetRetiredBigEndianProposed(true); - E_TransferSyntax xfer = dataset.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - dataset.updateOriginalXfer(); - xfer = dataset.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - throw OrthancException(ErrorCode_BadFileFormat, - "Cannot determine the transfer syntax of the DICOM instance"); - } - } + DcmtkTranscoder transcoder; - if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer)) - { - throw OrthancException( - ErrorCode_BadFileFormat, - "Unsupported transfer syntax: " + boost::lexical_cast(xfer)); - } - - const char* a = NULL; - const char* b = NULL; + for (int j = 0; j < 2; j++) + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; - if (!dataset.findAndGetString(DCM_SOPClassUID, a).good() || - !dataset.findAndGetString(DCM_SOPInstanceUID, b).good() || - a == NULL || - b == NULL) + std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" + + std::string(GetTransferSyntaxUid(a)) + ".dcm"); + if (Orthanc::SystemToolbox::IsRegularFile(path)) { - throw OrthancException(ErrorCode_BadFileFormat, - "Missing SOP class/instance UID in DICOM instance"); - } - - sopClassUid_.assign(a); - sopInstanceUid_.assign(b); - } - - public: - DcmtkTranscoder(DcmFileFormat* dicom) // Takes ownership - { - Setup(dicom); - } + printf("\n======= %s\n", GetTransferSyntaxUid(a)); - DcmtkTranscoder(const void* dicom, - size_t size) - { - Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size)); - } - - virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE - { - return transferSyntax_; - } - - virtual std::string GetSopClassUid() ORTHANC_OVERRIDE - { - return sopClassUid_; - } - - virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE - { - return sopInstanceUid_; - } + std::string source; + Orthanc::SystemToolbox::ReadFile(source, path); - virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE - { - return index_->GetFramesCount(); - } - - virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE - { - assert(dicom_->getDataset() != NULL); - return DicomImageDecoder::Decode(*dicom_->getDataset(), frame); - } - - virtual void GetCompressedFrame(std::string& target, - unsigned int frame) ORTHANC_OVERRIDE - { -#if 1 - index_->GetRawFrame(target, frame); - printf("%d: %d\n", frame, target.size()); -#endif - -#if 1 - assert(dicom_->getDataset() != NULL); - DcmDataset& dataset = *dicom_->getDataset(); - - DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset); - - if (pixelSequence != NULL && - frame == 0 && - pixelSequence->card() != GetFramesCount() + 1) - { - printf("COMPRESSED\n"); - - // Check out "djcodecd.cc" - - printf("%d fragments\n", pixelSequence->card()); - - // Skip the first fragment, that is the offset table - for (unsigned long i = 1; ;i++) + std::string c, i; + try { - DcmPixelItem *fragment = NULL; - if (pixelSequence->getItem(fragment, i).good()) + scu.Transcode(c, i, transcoder, source.c_str(), source.size(), false, "", 0); + } + catch (OrthancException& e) + { + if (e.GetErrorCode() == ErrorCode_NotImplemented) { - printf("fragment %d %d\n", i, fragment->getLength()); + LOG(ERROR) << "cannot transcode " << GetTransferSyntaxUid(a); } else { - break; + throw e; } } } -#endif } - - virtual IDicomTranscoder* Transcode(std::set syntaxes, - bool allowNewSopInstanceUid) ORTHANC_OVERRIDE - { - throw OrthancException(ErrorCode_NotImplemented); - } - }; } - - -static bool Transcode(std::string& buffer, - DcmDataset& dataSet, - E_TransferSyntax xfer) +TEST(Toto, DISABLED_Transcode4) { - // Determine the transfer syntax which shall be used to write the - // information to the file. We always switch to the Little Endian - // syntax, with explicit length. - - // http://support.dcmtk.org/docs/dcxfer_8h-source.html - - - /** - * Note that up to Orthanc 0.7.1 (inclusive), the - * "EXS_LittleEndianExplicit" was always used to save the DICOM - * dataset into memory. We now keep the original transfer syntax - * (if available). - **/ - //E_TransferSyntax xfer = dataSet.getOriginalXfer(); - if (xfer == EXS_Unknown) - { - // No information about the original transfer syntax: This is - // most probably a DICOM dataset that was read from memory. - xfer = EXS_LittleEndianExplicit; - } - - E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength; - - // Create the meta-header information - DcmFileFormat ff(&dataSet); - ff.validateMetaInfo(xfer); - ff.removeInvalidGroups(); - - // Create a memory buffer with the proper size - { - const uint32_t estimatedSize = ff.calcElementLength(xfer, encodingType); // (*) - buffer.resize(estimatedSize); - } - - DcmOutputBufferStream ob(&buffer[0], buffer.size()); - - // Fill the memory buffer with the meta-header and the dataset - ff.transferInit(); - OFCondition c = ff.write(ob, xfer, encodingType, NULL, - /*opt_groupLength*/ EGL_recalcGL, - /*opt_paddingType*/ EPD_withoutPadding); - ff.transferEnd(); - - if (c.good()) - { - // The DICOM file is successfully written, truncate the target - // buffer if its size was overestimated by (*) - ob.flush(); - - size_t effectiveSize = static_cast(ob.tell()); - if (effectiveSize < buffer.size()) - { - buffer.resize(effectiveSize); - } - - return true; - } - else - { - // Error - buffer.clear(); - return false; - } -} + std::string source; + Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm"); -#include "dcmtk/dcmjpeg/djrploss.h" /* for DJ_RPLossy */ -#include "dcmtk/dcmjpeg/djrplol.h" /* for DJ_RPLossless */ - -#include - - -static void TestFile(const std::string& path) -{ - printf("** %s\n", path.c_str()); - - std::string s; - SystemToolbox::ReadFile(s, path); - - Orthanc::DcmtkTranscoder transcoder(s.c_str(), s.size()); - - printf("[%s] [%s] [%s] %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()), - transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(), - transcoder.GetFramesCount()); - - for (size_t i = 0; i < transcoder.GetFramesCount(); i++) - { - std::string f; - transcoder.GetCompressedFrame(f, i); - - if (i == 0) - { - static unsigned int i = 0; - char buf[1024]; - sprintf(buf, "/tmp/frame-%06d.dcm", i++); - printf(">> %s\n", buf); - Orthanc::SystemToolbox::WriteFile(f, buf); - } - } + std::unique_ptr toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size())); + const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto); + + DicomTransferSyntax sourceSyntax; + ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto)); - printf("\n"); -} - -TEST(Toto, Transcode) -{ - if (0) - { - OFLog::configure(OFLogger::DEBUG_LOG_LEVEL); - - std::string s; - //SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm"); - //SystemToolbox::ReadFile(s, "/home/jodogne/DICOM/Alain.dcm"); - SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/Brainix/Epi/IM-0001-0002.dcm"); - - std::unique_ptr dicom(FromDcmtkBridge::LoadFromMemoryBuffer(s.c_str(), s.size())); + DcmtkTranscoder transcoder; - // less /home/jodogne/Downloads/dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcxfer.h - printf(">> %d\n", dicom->getDataset()->getOriginalXfer()); // => 4 == EXS_JPEGProcess1 - - const DcmRepresentationParameter *p; - -#if 0 - E_TransferSyntax target = EXS_LittleEndianExplicit; - p = NULL; -#elif 1 - E_TransferSyntax target = EXS_JPEGProcess14SV1; - DJ_RPLossless rp_lossless(6, 0); - p = &rp_lossless; -#else - E_TransferSyntax target = EXS_JPEGProcess1; - DJ_RPLossy rp_lossy(90); // quality - p = &rp_lossy; -#endif - - ASSERT_TRUE(dicom->getDataset()->chooseRepresentation(target, p).good()); - ASSERT_TRUE(dicom->getDataset()->canWriteXfer(target)); + for (int i = 0; i <= DicomTransferSyntax_XML; i++) + { + DicomTransferSyntax a = (DicomTransferSyntax) i; + + std::set s; + s.insert(a); std::string t; - ASSERT_TRUE(Transcode(t, *dicom->getDataset(), target)); - SystemToolbox::WriteFile(s, "source.dcm"); - SystemToolbox::WriteFile(t, "target.dcm"); - } + IDicomTranscoder::DicomImage source, target; + source.AcquireParsed(dynamic_cast(toto->clone())); - if (1) - { - const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes"; - - for (boost::filesystem::directory_iterator it(PATH); - it != boost::filesystem::directory_iterator(); ++it) + if (!transcoder.Transcode(target, source, s, true)) + { + printf("**************** CANNOT: [%s] => [%s]\n", + GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a)); + } + else { - if (boost::filesystem::is_regular_file(it->status())) + DicomTransferSyntax targetSyntax; + ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed())); + + ASSERT_EQ(targetSyntax, a); + bool lossy = (a == DicomTransferSyntax_JPEGProcess1 || + a == DicomTransferSyntax_JPEGProcess2_4 || + a == DicomTransferSyntax_JPEGLSLossy); + + printf("SIZE: %lu\n", t.size()); + if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed())) { - TestFile(it->path().string()); + ASSERT_FALSE(lossy); + } + else + { + ASSERT_TRUE(lossy); } } - - TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm"); - TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm"); } } diff -r fe0e4ef52a72 -r 6e14f2da7c7e UnitTestsSources/MultiThreadingTests.cpp --- a/UnitTestsSources/MultiThreadingTests.cpp Wed May 06 08:40:48 2020 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Wed May 20 16:42:44 2020 +0200 @@ -62,6 +62,7 @@ #include "../OrthancServer/ServerJobs/ArchiveJob.h" #include "../OrthancServer/ServerJobs/DicomModalityStoreJob.h" +#include "../OrthancServer/ServerJobs/DicomMoveScuJob.h" #include "../OrthancServer/ServerJobs/MergeStudyJob.h" #include "../OrthancServer/ServerJobs/OrthancPeerStoreJob.h" #include "../OrthancServer/ServerJobs/ResourceModificationJob.h" @@ -1314,7 +1315,7 @@ DicomInstanceToStore toStore; toStore.SetParsedDicomFile(dicom); - return (context_->Store(id, toStore) == StoreStatus_Success); + return (context_->Store(id, toStore, StoreInstanceMode_Default) == StoreStatus_Success); } }; } @@ -1414,7 +1415,7 @@ modality.SetPortNumber(1000); modality.SetManufacturer(ModalityManufacturer_StoreScp); - StoreScuOperation operation(luaManager, "TEST", modality); + StoreScuOperation operation(GetContext(), luaManager, "TEST", modality); ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation)); operation.Serialize(s); @@ -1514,11 +1515,11 @@ job.reset(unserializer.UnserializeJob(s)); DicomModalityStoreJob& tmp = dynamic_cast(*job); - ASSERT_EQ("LOCAL", tmp.GetLocalAet()); - ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle()); - ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost()); - ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber()); - ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetParameters().GetRemoteModality().GetManufacturer()); ASSERT_TRUE(tmp.HasMoveOriginator()); ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet()); ASSERT_EQ(42, tmp.GetMoveOriginatorId()); @@ -1548,6 +1549,30 @@ ASSERT_EQ("username", tmp.GetPeer().GetUsername()); ASSERT_EQ("password", tmp.GetPeer().GetPassword()); ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled()); + ASSERT_FALSE(tmp.IsTranscode()); + ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException); + } + + { + OrthancPeerStoreJob job(GetContext()); + ASSERT_THROW(job.SetTranscode("nope"), OrthancException); + job.SetTranscode("1.2.840.10008.1.2.4.50"); + + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + ASSERT_TRUE(job.Serialize(s)); + } + + { + std::unique_ptr job; + job.reset(unserializer.UnserializeJob(s)); + + OrthancPeerStoreJob& tmp = dynamic_cast(*job); + ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl()); + ASSERT_EQ("", tmp.GetPeer().GetUsername()); + ASSERT_EQ("", tmp.GetPeer().GetPassword()); + ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled()); + ASSERT_TRUE(tmp.IsTranscode()); + ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax()); } // ResourceModificationJob @@ -1559,7 +1584,8 @@ ResourceModificationJob job(GetContext()); job.SetModification(modification.release(), ResourceType_Patient, true); job.SetOrigin(DicomInstanceOrigin::FromLua()); - + + job.AddTrailingStep(); // Necessary since 1.7.0 ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); ASSERT_TRUE(job.Serialize(s)); } @@ -1570,10 +1596,33 @@ ResourceModificationJob& tmp = dynamic_cast(*job); ASSERT_TRUE(tmp.IsAnonymization()); + ASSERT_FALSE(tmp.IsTranscode()); + ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException); ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin()); ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION)); } + { + ResourceModificationJob job(GetContext()); + ASSERT_THROW(job.SetTranscode("nope"), OrthancException); + job.SetTranscode(DicomTransferSyntax_JPEGProcess1); + + job.AddTrailingStep(); // Necessary since 1.7.0 + ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); + ASSERT_TRUE(job.Serialize(s)); + } + + { + std::unique_ptr job; + job.reset(unserializer.UnserializeJob(s)); + + ResourceModificationJob& tmp = dynamic_cast(*job); + ASSERT_FALSE(tmp.IsAnonymization()); + ASSERT_TRUE(tmp.IsTranscode()); + ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax()); + ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin()); + } + // SplitStudyJob std::string instance; @@ -1902,6 +1951,7 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } s = Json::nullValue; @@ -1932,6 +1982,7 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } s["Port"] = "46"; @@ -1998,6 +2049,7 @@ ASSERT_EQ(104u, modality.GetPortNumber()); ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } { @@ -2007,6 +2059,7 @@ s["AET"] = "AET"; s["Host"] = "host"; s["Port"] = "104"; + s["AllowTranscoding"] = false; RemoteModalityParameters modality(s); ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); @@ -2015,6 +2068,7 @@ ASSERT_EQ(104u, modality.GetPortNumber()); ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_FALSE(modality.IsTranscodingAllowed()); } { @@ -2032,5 +2086,143 @@ ASSERT_EQ(104u, modality.GetPortNumber()); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); + ASSERT_TRUE(modality.IsTranscodingAllowed()); } } + + +TEST_F(OrthancJobsSerialization, DicomAssociationParameters) +{ + Json::Value v; + + { + v = Json::objectValue; + DicomAssociationParameters p; + p.SerializeJob(v); + } + + { + DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v); + ASSERT_EQ("ORTHANC", p.GetLocalApplicationEntityTitle()); + ASSERT_EQ("ANY-SCP", p.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ(104u, p.GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("127.0.0.1", p.GetRemoteModality().GetHost()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), p.GetTimeout()); + } + + { + v = Json::objectValue; + DicomAssociationParameters p; + p.SetLocalApplicationEntityTitle("HELLO"); + p.SetRemoteApplicationEntityTitle("WORLD"); + p.SetRemotePort(42); + p.SetRemoteHost("MY_HOST"); + p.SetTimeout(43); + p.SerializeJob(v); + } + + { + DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v); + ASSERT_EQ("HELLO", p.GetLocalApplicationEntityTitle()); + ASSERT_EQ("WORLD", p.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ(42u, p.GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer()); + ASSERT_EQ("MY_HOST", p.GetRemoteModality().GetHost()); + ASSERT_EQ(43u, p.GetTimeout()); + } + + { + DicomModalityStoreJob job(GetContext()); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout()); + ASSERT_FALSE(job->HasMoveOriginator()); + ASSERT_THROW(job->GetMoveOriginatorAet(), OrthancException); + ASSERT_THROW(job->GetMoveOriginatorId(), OrthancException); + ASSERT_FALSE(job->HasStorageCommitment()); + } + + { + RemoteModalityParameters r; + r.SetApplicationEntityTitle("HELLO"); + r.SetPortNumber(42); + r.SetHost("MY_HOST"); + + DicomModalityStoreJob job(GetContext()); + job.SetLocalAet("WORLD"); + job.SetRemoteModality(r); + job.SetTimeout(43); + job.SetMoveOriginator("ORIGINATOR", 100); + job.EnableStorageCommitment(true); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(43u, job->GetParameters().GetTimeout()); + ASSERT_TRUE(job->HasMoveOriginator()); + ASSERT_EQ("ORIGINATOR", job->GetMoveOriginatorAet()); + ASSERT_EQ(100, job->GetMoveOriginatorId()); + ASSERT_TRUE(job->HasStorageCommitment()); + } + + { + DicomMoveScuJob job(GetContext()); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout()); + } + + { + RemoteModalityParameters r; + r.SetApplicationEntityTitle("HELLO"); + r.SetPortNumber(42); + r.SetHost("MY_HOST"); + + DicomMoveScuJob job(GetContext()); + job.SetLocalAet("WORLD"); + job.SetRemoteModality(r); + job.SetTimeout(43); + job.Serialize(v); + } + + { + OrthancJobUnserializer unserializer(GetContext()); + std::unique_ptr job( + dynamic_cast(unserializer.UnserializeJob(v))); + ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle()); + ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost()); + ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber()); + ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer()); + ASSERT_EQ(43u, job->GetParameters().GetTimeout()); + } +} diff -r fe0e4ef52a72 -r 6e14f2da7c7e UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Wed May 06 08:40:48 2020 +0200 +++ b/UnitTestsSources/ServerIndexTests.cpp Wed May 20 16:42:44 2020 +0200 @@ -726,7 +726,8 @@ std::map instanceMetadata; DicomInstanceToStore toStore; toStore.SetSummary(instance); - ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments)); + ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments, + false /* don't overwrite */)); ASSERT_EQ(5u, instanceMetadata.size()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_RemoteAet) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end()); @@ -803,7 +804,7 @@ DicomInstanceHasher hasher(instance); std::string id = hasher.HashInstance(); - context.GetIndex().SetOverwriteInstances(overwrite); + context.SetOverwriteInstances(overwrite); uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances; context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, @@ -819,7 +820,7 @@ ASSERT_EQ(id, toStore.GetHasher().HashInstance()); std::string id2; - ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore)); + ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore, StoreInstanceMode_Default)); ASSERT_EQ(id, id2); } @@ -854,7 +855,8 @@ toStore.SetOrigin(DicomInstanceOrigin::FromPlugins()); std::string id2; - ASSERT_EQ(overwrite ? StoreStatus_Success : StoreStatus_AlreadyStored, context.Store(id2, toStore)); + ASSERT_EQ(overwrite ? StoreStatus_Success : StoreStatus_AlreadyStored, + context.Store(id2, toStore, StoreInstanceMode_Default)); ASSERT_EQ(id, id2); }