Mercurial > hg > orthanc
changeset 1857:4f2386d0f326 dcmtk-3.6.1
integration mainline->dcmtk-3.6.1
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 02 Dec 2015 09:52:56 +0100 |
parents | c131566b8252 (current diff) 36ab170733d6 (diff) |
children | 483f26479743 |
files | Plugins/Samples/GdcmDecoding/CMakeLists.txt Plugins/Samples/GdcmDecoding/OrthancContext.cpp Plugins/Samples/GdcmDecoding/OrthancContext.h Plugins/Samples/GdcmDecoding/Plugin.cpp Resources/CMake/DcmtkConfiguration.cmake |
diffstat | 126 files changed, 4919 insertions(+), 1325 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Wed Nov 18 10:16:21 2015 +0100 +++ b/CMakeLists.txt Wed Dec 02 09:52:56 2015 +0100 @@ -31,6 +31,7 @@ SET(ENABLE_JPEG_LOSSLESS ON CACHE BOOL "Enable JPEG-LS (Lossless) decompression") SET(ENABLE_PLUGINS ON CACHE BOOL "Enable plugins") SET(BUILD_SERVE_FOLDERS ON CACHE BOOL "Build the ServeFolders plugin") +SET(BUILD_MODALITY_WORKLISTS ON CACHE BOOL "Build the sample plugin to serve modality worklists") # Advanced parameters to fine-tune linking against system libraries SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") @@ -179,6 +180,8 @@ OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/ParsedDicomFile.cpp OrthancServer/QueryRetrieveHandler.cpp + OrthancServer/Search/HierarchicalMatcher.cpp + OrthancServer/Search/IFindConstraint.cpp OrthancServer/Search/LookupIdentifierQuery.cpp OrthancServer/Search/LookupResource.cpp OrthancServer/Search/SetOfResources.cpp @@ -373,6 +376,7 @@ -DORTHANC_VERSION="${ORTHANC_VERSION}" -DORTHANC_DATABASE_VERSION=${ORTHANC_DATABASE_VERSION} -DORTHANC_ENABLE_LOGGING=1 + -DORTHANC_MAXIMUM_TAG_LENGTH=256 ) list(LENGTH OPENSSL_SOURCES OPENSSL_SOURCES_LENGTH) @@ -509,6 +513,49 @@ ##################################################################### +## Build the "ModalityWorklists" plugin +##################################################################### + +if (ENABLE_PLUGINS AND BUILD_MODALITY_WORKLISTS) + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py + ${ORTHANC_VERSION} ModalityWorklists ModalityWorklists.dll "Sample Orthanc plugin to serve modality worklists" + ERROR_VARIABLE Failure + OUTPUT_FILE ${AUTOGENERATED_DIR}/ModalityWorklists.rc + ) + + if (Failure) + message(FATAL_ERROR "Error while computing the version information: ${Failure}") + endif() + + add_definitions(-DMODALITY_WORKLISTS_VERSION="${ORTHANC_VERSION}") + + include_directories(${CMAKE_SOURCE_DIR}/Plugins/Include) + + add_library(ModalityWorklists SHARED + ${BOOST_SOURCES} + ${JSONCPP_SOURCES} + Plugins/Samples/ModalityWorklists/Plugin.cpp + ${AUTOGENERATED_DIR}/ModalityWorklists.rc + ) + + set_target_properties( + ModalityWorklists PROPERTIES + VERSION ${ORTHANC_VERSION} + SOVERSION ${ORTHANC_VERSION} + ) + + install( + TARGETS ModalityWorklists + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + ) +endif() + + + +##################################################################### ## Generate the documentation if Doxygen is present #####################################################################
--- a/Core/DicomFormat/DicomMap.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/DicomFormat/DicomMap.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -61,7 +61,11 @@ DicomTag(0x0020, 0x0010), // StudyID DICOM_TAG_STUDY_DESCRIPTION, DICOM_TAG_ACCESSION_NUMBER, - DICOM_TAG_STUDY_INSTANCE_UID + DICOM_TAG_STUDY_INSTANCE_UID, + DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION, // New in db v6 + DICOM_TAG_INSTITUTION_NAME, // New in db v6 + DICOM_TAG_REQUESTING_PHYSICIAN, // New in db v6 + DICOM_TAG_REFERRING_PHYSICIAN_NAME // New in db v6 }; static DicomTag seriesTags[] = @@ -83,7 +87,12 @@ DICOM_TAG_NUMBER_OF_SLICES, DICOM_TAG_NUMBER_OF_TIME_SLICES, DICOM_TAG_SERIES_INSTANCE_UID, - DICOM_TAG_IMAGE_ORIENTATION_PATIENT // New in db v6 + DICOM_TAG_IMAGE_ORIENTATION_PATIENT, // New in db v6 + DICOM_TAG_SERIES_TYPE, // New in db v6 + DICOM_TAG_OPERATOR_NAME, // New in db v6 + DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, // New in db v6 + DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION, // New in db v6 + DICOM_TAG_CONTRAST_BOLUS_AGENT // New in db v6 }; static DicomTag instanceTags[] = @@ -96,7 +105,8 @@ DICOM_TAG_NUMBER_OF_FRAMES, DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, DICOM_TAG_SOP_INSTANCE_UID, - DICOM_TAG_IMAGE_POSITION_PATIENT // New in db v6 + DICOM_TAG_IMAGE_POSITION_PATIENT, // New in db v6 + DICOM_TAG_IMAGE_COMMENTS // New in db v6 }; @@ -296,6 +306,12 @@ SetupFindTemplate(result, studyTags, sizeof(studyTags) / sizeof(DicomTag)); result.SetValue(DICOM_TAG_ACCESSION_NUMBER, ""); result.SetValue(DICOM_TAG_PATIENT_ID, ""); + + // These main DICOM tags are only indirectly related to the + // General Study Module, remove them + result.Remove(DICOM_TAG_INSTITUTION_NAME); + result.Remove(DICOM_TAG_REQUESTING_PHYSICIAN); + result.Remove(DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION); } void DicomMap::SetupFindSeriesTemplate(DicomMap& result) @@ -315,6 +331,9 @@ result.Remove(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS); result.Remove(DICOM_TAG_NUMBER_OF_TIME_SLICES); result.Remove(DICOM_TAG_IMAGE_ORIENTATION_PATIENT); + result.Remove(DICOM_TAG_SERIES_TYPE); + result.Remove(DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION); + result.Remove(DICOM_TAG_CONTRAST_BOLUS_AGENT); } void DicomMap::SetupFindInstanceTemplate(DicomMap& result)
--- a/Core/DicomFormat/DicomTag.h Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/DicomFormat/DicomTag.h Wed Dec 02 09:52:56 2015 +0100 @@ -66,6 +66,11 @@ return element_; } + bool IsPrivate() const + { + return group_ % 2 == 1; + } + const char* GetMainTagsName() const; bool operator< (const DicomTag& other) const; @@ -152,4 +157,16 @@ static const DicomTag DICOM_TAG_SERIES_TIME(0x0008, 0x0031); static const DicomTag DICOM_TAG_STUDY_DATE(0x0008, 0x0020); static const DicomTag DICOM_TAG_STUDY_TIME(0x0008, 0x0030); + + // Various tags + static const DicomTag DICOM_TAG_SERIES_TYPE(0x0054, 0x1000); + static const DicomTag DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION(0x0032, 0x1060); + static const DicomTag DICOM_TAG_INSTITUTION_NAME(0x0008, 0x0080); + static const DicomTag DICOM_TAG_REQUESTING_PHYSICIAN(0x0032, 0x1032); + static const DicomTag DICOM_TAG_REFERRING_PHYSICIAN_NAME(0x0008, 0x0090); + static const DicomTag DICOM_TAG_OPERATOR_NAME(0x0008, 0x1070); + static const DicomTag DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION(0x0040, 0x0254); + static const DicomTag DICOM_TAG_IMAGE_COMMENTS(0x0020, 0x4000); + static const DicomTag DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION(0x0018, 0x1400); + static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010); }
--- a/Core/Enumerations.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/Enumerations.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -325,6 +325,9 @@ case ErrorCode_CannotOrderSlices: return "Unable to order the slices of the series"; + case ErrorCode_NoWorklistHandler: + return "No request handler factory for DICOM C-Find Modality SCP"; + default: if (error >= ErrorCode_START_PLUGINS) { @@ -678,8 +681,8 @@ case RequestOrigin_DicomProtocol: return "DicomProtocol"; - case RequestOrigin_Http: - return "Http"; + case RequestOrigin_RestApi: + return "RestApi"; case RequestOrigin_Plugins: return "Plugins";
--- a/Core/Enumerations.h Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/Enumerations.h Wed Dec 02 09:52:56 2015 +0100 @@ -138,6 +138,7 @@ ErrorCode_DatabaseNotInitialized = 2038 /*!< Plugin trying to call the database during its initialization */, ErrorCode_SslDisabled = 2039 /*!< Orthanc has been built without SSL support */, ErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, + ErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, ErrorCode_START_PLUGINS = 1000000 }; @@ -369,7 +370,7 @@ { RequestOrigin_Unknown, RequestOrigin_DicomProtocol, - RequestOrigin_Http, + RequestOrigin_RestApi, RequestOrigin_Plugins, RequestOrigin_Lua };
--- a/Core/HttpServer/HttpOutput.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/HttpServer/HttpOutput.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -442,7 +442,7 @@ multipartBoundary_ = Toolbox::GenerateUuid(); multipartContentType_ = contentType; - header += "Content-Type: multipart/related; type=multipart/" + subType + "; boundary=" + multipartBoundary_ + "\r\n\r\n"; + header += "Content-Type: multipart/" + subType + "; type=" + contentType + "; boundary=" + multipartBoundary_ + "\r\n\r\n"; stream_.Send(true, header.c_str(), header.size()); state_ = State_WritingMultipart; @@ -451,10 +451,10 @@ void HttpOutput::StateMachine::SendMultipartItem(const void* item, size_t length) { - std::string header = "--" + multipartBoundary_ + "\n"; - header += "Content-Type: " + multipartContentType_ + "\n"; - header += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\n"; - header += "MIME-Version: 1.0\n\n"; + std::string header = "--" + multipartBoundary_ + "\r\n"; + header += "Content-Type: " + multipartContentType_ + "\r\n"; + header += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n"; + header += "MIME-Version: 1.0\r\n\r\n"; stream_.Send(false, header.c_str(), header.size()); @@ -463,7 +463,7 @@ stream_.Send(false, item, length); } - stream_.Send(false, "\n", 1); + stream_.Send(false, "\r\n", 1); } @@ -478,7 +478,7 @@ // closed the connection. Such an error is ignored. try { - std::string header = "--" + multipartBoundary_ + "--\n"; + std::string header = "--" + multipartBoundary_ + "--\r\n"; stream_.Send(false, header.c_str(), header.size()); } catch (OrthancException&)
--- a/Core/HttpServer/MongooseServer.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/HttpServer/MongooseServer.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -727,7 +727,7 @@ { if (that->HasHandler()) { - found = that->GetHandler().Handle(output, RequestOrigin_Http, remoteIp, username.c_str(), + found = that->GetHandler().Handle(output, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers, argumentsGET, body.c_str(), body.size()); } }
--- a/Core/MultiThreading/RunnableWorkersPool.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/MultiThreading/RunnableWorkersPool.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -105,7 +105,7 @@ { pimpl_->continue_ = true; - if (countWorkers <= 0) + if (countWorkers == 0) { throw OrthancException(ErrorCode_ParameterOutOfRange); }
--- a/Core/Toolbox.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/Toolbox.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -494,16 +494,16 @@ void Toolbox::ComputeMD5(std::string& result, const void* data, - size_t length) + size_t size) { md5_state_s state; md5_init(&state); - if (length > 0) + if (size > 0) { md5_append(&state, reinterpret_cast<const md5_byte_t*>(data), - static_cast<int>(length)); + static_cast<int>(size)); } md5_byte_t actualHash[16]; @@ -554,6 +554,14 @@ } # endif + + void Toolbox::EncodeDataUriScheme(std::string& result, + const std::string& mime, + const std::string& content) + { + result = "data:" + mime + ";base64," + base64_encode(content); + } + #endif @@ -751,14 +759,16 @@ return result; } + void Toolbox::ComputeSHA1(std::string& result, - const std::string& data) + const void* data, + size_t size) { boost::uuids::detail::sha1 sha1; - if (data.size() > 0) + if (size > 0) { - sha1.process_bytes(&data[0], data.size()); + sha1.process_bytes(data, size); } unsigned int digest[5]; @@ -777,6 +787,20 @@ digest[4]); } + void Toolbox::ComputeSHA1(std::string& result, + const std::string& data) + { + if (data.size() > 0) + { + ComputeSHA1(result, data.c_str(), data.size()); + } + else + { + ComputeSHA1(result, NULL, 0); + } + } + + bool Toolbox::IsSHA1(const char* str, size_t size) {
--- a/Core/Toolbox.h Wed Nov 18 10:16:21 2015 +0100 +++ b/Core/Toolbox.h Wed Dec 02 09:52:56 2015 +0100 @@ -100,12 +100,16 @@ void ComputeMD5(std::string& result, const void* data, - size_t length); + size_t size); #endif void ComputeSHA1(std::string& result, const std::string& data); + void ComputeSHA1(std::string& result, + const void* data, + size_t size); + bool IsSHA1(const char* str, size_t size); @@ -123,6 +127,10 @@ std::string& content, const std::string& source); # endif + + void EncodeDataUriScheme(std::string& result, + const std::string& mime, + const std::string& content); #endif std::string GetPathToExecutable();
--- a/NEWS Wed Nov 18 10:16:21 2015 +0100 +++ b/NEWS Wed Dec 02 09:52:56 2015 +0100 @@ -1,30 +1,60 @@ Pending changes in the mainline =============================== -* Full indexation of the patient/study tags to speed up searches and C-Find -* Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive") -* "/tools/create-dicom": Support of binary tags encoded using data URI scheme -* "/tools/create-dicom": Support of hierarchical structures (creation of sequences) +Major +----- + +* Experimental support of DICOM C-Find SCP for modality worklists through plugins +* Support of DICOM C-Find SCU for modality worklists ("/modalities/{dicom}/find-worklist") + +REST API +-------- + +* New URIs: + - "/series/.../ordered-slices" to order the slices of a 2D+t or 3D series + - "/tools/shutdown" to stop Orthanc from the REST API + - ".../compress", ".../uncompress" and ".../is-compressed" for attachments + - "/tools/create-archive" to create ZIP from a set of resources + - "/tools/create-media" to create ZIP+DICOMDIR from a set of resources + - "/instances/.../header" to get the meta information (header) of the DICOM instance +* "/tools/create-dicom": + - Support of binary tags encoded using data URI scheme + - Support of hierarchical structures (creation of sequences) + - Create tags with unknown VR * "/modify" can insert/modify sequences -* "/series/.../ordered-slices" to order the slices of a 2D+t or 3D image -* New URI "/tools/shutdown" to stop Orthanc from the REST API -* New URIs for attachments: ".../compress", ".../uncompress" and ".../is-compressed" -* New configuration option: "Dictionary" to declare custom DICOM tags +* ".../preview" and ".../image-uint8" can return JPEG images if the HTTP Accept Header asks so +* "Origin" metadata for the instances + +Minor +----- + +* New configuration options: + - "UnknownSopClassAccepted" to disable promiscuous mode (accept unknown SOP class UID) + - New configuration option: "Dictionary" to declare custom DICOM tags +* Add ".dcm" suffix to files in ZIP archives (cf. URI ".../archive") * MIME content type can be associated to custom attachments (cf. "UserContentType") -* New URIs "/tools/create-archive" and "/tools/create-media" to create ZIP/DICOMDIR - from a set of resources -* ".../preview" and ".../image-uint8" can return JPEG images if the HTTP Accept Header asks so Plugins ------- -* New function "OrthancPluginRegisterErrorCode()" to declare custom error codes -* New function "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags -* New function "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API -* New "OrthancStarted", "OrthancStopped", "UpdatedAttachment" - and "UpdatedMetadata" events in change callbacks +* New functions: + - "OrthancPluginRegisterDecodeImageCallback()" to replace the built-in image decoder + - "OrthancPluginDicomInstanceToJson()" to convert DICOM to JSON + - "OrthancPluginDicomBufferToJson()" to convert DICOM to JSON + - "OrthancPluginRegisterErrorCode()" to declare custom error codes + - "OrthancPluginRegisterDictionaryTag()" to declare custom DICOM tags + - "OrthancPluginRestApiGet2()" to provide HTTP headers when calling Orthanc API + - "OrthancPluginGetInstanceOrigin()" to know through which mechanism an instance was received + - "OrthancPluginCreateImage()" and "OrthancPluginCreateImageAccessor()" to create images + - "OrthancPluginDecodeDicomImage()" to decode DICOM images + - "OrthancPluginComputeMd5()" and "OrthancPluginComputeSha1()" to compute MD5/SHA-1 hash +* New events in change callbacks: + - "OrthancStarted" + - "OrthancStopped" + - "UpdatedAttachment" + - "UpdatedMetadata" * "/system" URI gives information about the plugins used for storage area and DB back-end -* Plugin callbacks should now return explicit "OrthancPluginErrorCode" instead of integers +* Plugin callbacks must now return explicit "OrthancPluginErrorCode" (instead of integers) Lua --- @@ -34,13 +64,16 @@ Maintenance ----------- -* Full refactoring of the searching features +* Full indexation of the patient/study tags to speed up searches and C-Find +* Many refactorings, notably of the searching features and of the image decoding * C-Move SCP for studies using AccessionNumber tag * Fix issue 4 (C-Store Association not renegotiated on Specific-to-specific transfer syntax change) -* "/tools/create-dicom" can create tags with unknown VR +* Fix formatting of multipart HTTP answers * "--logdir" flag creates a single log file instead of 3 separate files for errors/warnings/infos * "--errors" flag lists the error codes that could be returned by Orthanc * Under Windows, the exit status of Orthanc corresponds to the encountered error code +* New "AgfaImpax", "EFilm2" and "Vitrea" modality manufacturers +* Upgrade to Boost 1.59.0 for static builds Version 0.9.4 (2015/09/16)
--- a/OrthancServer/DatabaseWrapper.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DatabaseWrapper.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -256,12 +256,20 @@ } - DatabaseWrapper::DatabaseWrapper(const std::string& path) : listener_(NULL), base_(db_) + DatabaseWrapper::DatabaseWrapper(const std::string& path) : + listener_(NULL), + base_(db_), + signalRemainingAncestor_(NULL), + version_(0) { db_.Open(path); } - DatabaseWrapper::DatabaseWrapper() : listener_(NULL), base_(db_) + DatabaseWrapper::DatabaseWrapper() : + listener_(NULL), + base_(db_), + signalRemainingAncestor_(NULL), + version_(0) { db_.OpenInMemory(); }
--- a/OrthancServer/DicomDirWriter.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomDirWriter.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -527,7 +527,7 @@ path = directory + '\\' + filename; } - DcmFileFormat& fileFormat = *reinterpret_cast<DcmFileFormat*>(dicom.GetDcmtkObject()); + DcmFileFormat& fileFormat = dicom.GetDcmtkObject(); DcmDirectoryRecord* instance; bool isNewInstance = pimpl_->CreateResource(instance, ResourceType_Instance, fileFormat, filename.c_str(), path.c_str());
--- a/OrthancServer/DicomInstanceToStore.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomInstanceToStore.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -41,12 +41,6 @@ namespace Orthanc { - static DcmDataset& GetDataset(ParsedDicomFile& file) - { - return *reinterpret_cast<DcmFileFormat*>(file.GetDcmtkObject())->getDataset(); - } - - void DicomInstanceToStore::AddMetadata(ResourceType level, MetadataType metadata, const std::string& value) @@ -69,17 +63,23 @@ { if (!parsed_.HasContent()) { - throw OrthancException(ErrorCode_NotImplemented); + if (!summary_.HasContent()) + { + throw OrthancException(ErrorCode_NotImplemented); + } + else + { + parsed_.TakeOwnership(new ParsedDicomFile(summary_.GetConstContent())); + } } - else + + // Serialize the parsed DICOM file + buffer_.Allocate(); + if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset())) { - // Serialize the parsed DICOM file - buffer_.Allocate(); - if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), GetDataset(parsed_.GetContent()))) - { - LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; - throw OrthancException(ErrorCode_InternalError); - } + LOG(ERROR) << "Unable to serialize a DICOM file to a memory buffer"; + throw OrthancException(ErrorCode_InternalError); } } @@ -103,16 +103,18 @@ if (!summary_.HasContent()) { summary_.Allocate(); - FromDcmtkBridge::Convert(summary_.GetContent(), GetDataset(parsed_.GetContent())); + FromDcmtkBridge::Convert(summary_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset()); } if (!json_.HasContent()) { json_.Allocate(); - FromDcmtkBridge::ToJson(json_.GetContent(), GetDataset(parsed_.GetContent()), + FromDcmtkBridge::ToJson(json_.GetContent(), + *parsed_.GetContent().GetDcmtkObject().getDataset(), DicomToJsonFormat_Full, DicomToJsonFlags_Default, - 256 /* max string length */); + ORTHANC_MAXIMUM_TAG_LENGTH); } } @@ -200,7 +202,7 @@ break; } - case RequestOrigin_Http: + case RequestOrigin_RestApi: { result["RemoteIp"] = remoteIp_; result["Username"] = httpUsername_; @@ -234,7 +236,7 @@ { origin_ = call.GetRequestOrigin(); - if (origin_ == RequestOrigin_Http) + if (origin_ == RequestOrigin_RestApi) { remoteIp_ = call.GetRemoteIp(); httpUsername_ = call.GetUsername(); @@ -244,7 +246,7 @@ void DicomInstanceToStore::SetHttpOrigin(const char* remoteIp, const char* username) { - origin_ = RequestOrigin_Http; + origin_ = RequestOrigin_RestApi; remoteIp_ = remoteIp; httpUsername_ = username; }
--- a/OrthancServer/DicomInstanceToStore.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomInstanceToStore.h Wed Dec 02 09:52:56 2015 +0100 @@ -171,6 +171,11 @@ void SetPluginsOrigin(); + RequestOrigin GetRequestOrigin() const + { + return origin_; + } + const char* GetRemoteAet() const; void SetBuffer(const std::string& dicom)
--- a/OrthancServer/DicomModification.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomModification.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -161,7 +161,7 @@ removals_.erase(tag); RemoveInternal(tag); - if (FromDcmtkBridge::IsPrivateTag(tag)) + if (tag.IsPrivate()) { privateTagsToKeep_.insert(tag); }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -34,25 +34,166 @@ #include "DicomFindAnswers.h" #include "../FromDcmtkBridge.h" +#include "../ToDcmtkBridge.h" +#include "../../Core/OrthancException.h" + +#include <memory> +#include <dcmtk/dcmdata/dcfilefo.h> +#include <boost/noncopyable.hpp> + namespace Orthanc { + class DicomFindAnswers::Answer : public boost::noncopyable + { + private: + ParsedDicomFile* dicom_; + DicomMap* map_; + + void CleanupDicom() + { + if (dicom_ != NULL) + { + dicom_->Remove(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID); + dicom_->Remove(DICOM_TAG_SOP_INSTANCE_UID); + } + } + + public: + Answer(ParsedDicomFile& dicom) : + dicom_(dicom.Clone()), + map_(NULL) + { + CleanupDicom(); + } + + Answer(const char* dicom, + size_t size) : + dicom_(new ParsedDicomFile(dicom, size)), + map_(NULL) + { + CleanupDicom(); + } + + Answer(const DicomMap& map) : + dicom_(NULL), + map_(map.Clone()) + { + } + + ~Answer() + { + if (dicom_ != NULL) + { + delete dicom_; + } + + if (map_ != NULL) + { + delete map_; + } + } + + ParsedDicomFile& GetDicomFile() + { + if (dicom_ == NULL) + { + assert(map_ != NULL); + dicom_ = new ParsedDicomFile(*map_); + } + + return *dicom_; + } + + DcmDataset* ExtractDcmDataset() const + { + if (dicom_ != NULL) + { + return new DcmDataset(*dicom_->GetDcmtkObject().getDataset()); + } + else + { + assert(map_ != NULL); + return ToDcmtkBridge::Convert(*map_); + } + } + }; + + void DicomFindAnswers::Clear() { - for (size_t i = 0; i < items_.size(); i++) + for (size_t i = 0; i < answers_.size(); i++) { - delete items_[i]; + assert(answers_[i] != NULL); + delete answers_[i]; + } + + answers_.clear(); + } + + + void DicomFindAnswers::Reserve(size_t size) + { + if (size > answers_.size()) + { + answers_.reserve(size); } } - void DicomFindAnswers::Reserve(size_t size) + + void DicomFindAnswers::Add(const DicomMap& map) + { + answers_.push_back(new Answer(map)); + } + + + void DicomFindAnswers::Add(ParsedDicomFile& dicom) { - if (size > items_.size()) + answers_.push_back(new Answer(dicom)); + } + + + void DicomFindAnswers::Add(const char* dicom, + size_t size) + { + answers_.push_back(new Answer(dicom, size)); + } + + + DicomFindAnswers::Answer& DicomFindAnswers::GetAnswerInternal(size_t index) const + { + if (index < answers_.size()) { - items_.reserve(size); + return *answers_.at(index); + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); } } + + ParsedDicomFile& DicomFindAnswers::GetAnswer(size_t index) const + { + return GetAnswerInternal(index).GetDicomFile(); + } + + + DcmDataset* DicomFindAnswers::ExtractDcmDataset(size_t index) const + { + return GetAnswerInternal(index).ExtractDcmDataset(); + } + + + void DicomFindAnswers::ToJson(Json::Value& target, + size_t index, + bool simplify) const + { + DicomToJsonFormat format = (simplify ? DicomToJsonFormat_Simple : DicomToJsonFormat_Full); + GetAnswer(index).ToJson(target, format, DicomToJsonFlags_None, 0); + } + + void DicomFindAnswers::ToJson(Json::Value& target, bool simplify) const { @@ -60,8 +201,8 @@ for (size_t i = 0; i < GetSize(); i++) { - Json::Value answer(Json::objectValue); - FromDcmtkBridge::ToJson(answer, GetAnswer(i), simplify); + Json::Value answer; + ToJson(answer, i, simplify); target.append(answer); } }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h Wed Dec 02 09:52:56 2015 +0100 @@ -32,19 +32,25 @@ #pragma once -#include "../../Core/DicomFormat/DicomMap.h" - -#include <vector> -#include <json/json.h> +#include "../ParsedDicomFile.h" namespace Orthanc { - class DicomFindAnswers + class DicomFindAnswers : public boost::noncopyable { private: - std::vector<DicomMap*> items_; + class Answer; + + std::vector<Answer*> answers_; + bool complete_; + + Answer& GetAnswerInternal(size_t index) const; public: + DicomFindAnswers() : complete_(true) + { + } + ~DicomFindAnswers() { Clear(); @@ -54,22 +60,37 @@ void Reserve(size_t index); - void Add(const DicomMap& map) - { - items_.push_back(map.Clone()); - } + void Add(const DicomMap& map); + + void Add(ParsedDicomFile& dicom); + + void Add(const char* dicom, + size_t size); size_t GetSize() const { - return items_.size(); + return answers_.size(); } - const DicomMap& GetAnswer(size_t index) const - { - return *items_.at(index); - } + ParsedDicomFile& GetAnswer(size_t index) const; + + DcmDataset* ExtractDcmDataset(size_t index) const; void ToJson(Json::Value& target, bool simplify) const; + + void ToJson(Json::Value& target, + size_t index, + bool simplify) const; + + bool IsComplete() const + { + return complete_; + } + + void SetComplete(bool isComplete) + { + complete_ = isComplete; + } }; }
--- a/OrthancServer/DicomProtocol/DicomServer.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomServer.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -94,6 +94,7 @@ findRequestHandlerFactory_ = NULL; moveRequestHandlerFactory_ = NULL; storeRequestHandlerFactory_ = NULL; + worklistRequestHandlerFactory_ = NULL; applicationEntityFilter_ = NULL; checkCalledAet_ = true; clientTimeout_ = 30; @@ -245,6 +246,29 @@ } } + void DicomServer::SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& factory) + { + Stop(); + worklistRequestHandlerFactory_ = &factory; + } + + bool DicomServer::HasWorklistRequestHandlerFactory() const + { + return (worklistRequestHandlerFactory_ != NULL); + } + + IWorklistRequestHandlerFactory& DicomServer::GetWorklistRequestHandlerFactory() const + { + if (HasWorklistRequestHandlerFactory()) + { + return *worklistRequestHandlerFactory_; + } + else + { + throw OrthancException(ErrorCode_NoWorklistHandler); + } + } + void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory) { Stop();
--- a/OrthancServer/DicomProtocol/DicomServer.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomServer.h Wed Dec 02 09:52:56 2015 +0100 @@ -35,6 +35,7 @@ #include "IFindRequestHandlerFactory.h" #include "IMoveRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" +#include "IWorklistRequestHandlerFactory.h" #include "IApplicationEntityFilter.h" #include <boost/shared_ptr.hpp> @@ -53,11 +54,11 @@ std::string aet_; uint16_t port_; bool continue_; - bool started_; uint32_t clientTimeout_; IFindRequestHandlerFactory* findRequestHandlerFactory_; IMoveRequestHandlerFactory* moveRequestHandlerFactory_; IStoreRequestHandlerFactory* storeRequestHandlerFactory_; + IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; IApplicationEntityFilter* applicationEntityFilter_; static void ServerThread(DicomServer* server); @@ -91,6 +92,10 @@ bool HasStoreRequestHandlerFactory() const; IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const; + void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler); + bool HasWorklistRequestHandlerFactory() const; + IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const; + void SetApplicationEntityFilter(IApplicationEntityFilter& handler); bool HasApplicationEntityFilter() const; IApplicationEntityFilter& GetApplicationEntityFilter() const;
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -353,7 +353,8 @@ struct FindPayload { DicomFindAnswers* answers; - std::string level; + const char* level; + bool isWorklist; }; } @@ -371,15 +372,23 @@ if (responseIdentifiers != NULL) { - DicomMap m; - FromDcmtkBridge::Convert(m, *responseIdentifiers); - - if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + if (payload.isWorklist) + { + ParsedDicomFile answer(*responseIdentifiers); + payload.answers->Add(answer); + } + else { - m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level); + DicomMap m; + FromDcmtkBridge::Convert(m, *responseIdentifiers); + + if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) + { + m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level); + } + + payload.answers->Add(m); } - - payload.answers->Add(m); } } @@ -438,12 +447,15 @@ { switch (manufacturer) { + case ModalityManufacturer_AgfaImpax: case ModalityManufacturer_SyngoVia: { std::auto_ptr<DicomMap> fix(fields.Clone()); // This issue for Syngo.Via and its solution was reported by - // Emsy Chan by private mail on June 17th, 2015. + // 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. std::set<DicomTag> tags; fix->GetTags(tags); @@ -474,6 +486,52 @@ } + static void ExecuteFind(DicomFindAnswers& answers, + T_ASC_Association* association, + DcmDataset* dataset, + const char* sopClass, + bool isWorklist, + const char* level, + uint32_t dimseTimeout) + { + 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); + } + + T_DIMSE_C_FindRQ request; + memset(&request, 0, sizeof(request)); + request.MessageID = association->nextMsgID++; + strcpy(request.AffectedSOPClassUID, sopClass); + request.DataSetType = DIMSE_DATASET_PRESENT; + request.Priority = DIMSE_PRIORITY_MEDIUM; + + T_DIMSE_C_FindRSP response; + DcmDataset* statusDetail = NULL; + OFCondition cond = DIMSE_findUser(association, presID, &request, dataset, + FindCallback, &payload, + /*opt_blockMode*/ DIMSE_BLOCKING, + /*opt_dimse_timeout*/ dimseTimeout, + &response, &statusDetail); + + if (statusDetail) + { + delete statusDetail; + } + + Check(cond); + } + + void DicomUserConnection::Find(DicomFindAnswers& result, ResourceType level, const DicomMap& originalFields) @@ -483,34 +541,32 @@ CheckIsOpen(); - FindPayload payload; - payload.answers = &result; + std::auto_ptr<DcmDataset> dataset(ConvertQueryFields(fields, manufacturer_)); + const char* clevel = NULL; + const char* sopClass = NULL; - std::auto_ptr<DcmDataset> dataset(ConvertQueryFields(fields, manufacturer_)); - - const char* sopClass; switch (level) { case ResourceType_Patient: - payload.level = "PATIENT"; + clevel = "PATIENT"; DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT"); sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; break; case ResourceType_Study: - payload.level = "STUDY"; + clevel = "STUDY"; DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY"); sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; case ResourceType_Series: - payload.level = "SERIES"; + clevel = "SERIES"; DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES"); sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; case ResourceType_Instance: - payload.level = "INSTANCE"; + clevel = "INSTANCE"; if (manufacturer_ == ModalityManufacturer_ClearCanvas || manufacturer_ == ModalityManufacturer_Dcm4Chee) { @@ -565,34 +621,8 @@ 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_DicomFindUnavailable); - } - - T_DIMSE_C_FindRQ request; - memset(&request, 0, sizeof(request)); - request.MessageID = pimpl_->assoc_->nextMsgID++; - strcpy(request.AffectedSOPClassUID, sopClass); - request.DataSetType = DIMSE_DATASET_PRESENT; - request.Priority = DIMSE_PRIORITY_MEDIUM; - - T_DIMSE_C_FindRSP response; - DcmDataset* statusDetail = NULL; - OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(), - FindCallback, &payload, - /*opt_blockMode*/ DIMSE_BLOCKING, - /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, - &response, &statusDetail); - - if (statusDetail) - { - delete statusDetail; - } - - Check(cond); + assert(clevel != NULL && sopClass != NULL); + ExecuteFind(result, pimpl_->assoc_, dataset.get(), sopClass, false, clevel, pimpl_->dimseTimeout_); } @@ -685,13 +715,14 @@ defaultStorageSOPClasses_.clear(); // Copy the short list of storage SOP classes from DCMTK, making - // room for the 4 SOP classes reserved for C-ECHO, C-FIND, C-MOVE. + // room for the 5 SOP classes reserved for C-ECHO, C-FIND, C-MOVE at (**). std::set<std::string> 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++) @@ -721,11 +752,12 @@ pimpl_->params_ = NULL; pimpl_->assoc_ = NULL; - // SOP classes for C-ECHO, C-FIND and C-MOVE + // 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(); } @@ -1101,4 +1133,15 @@ 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_); + } }
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/DicomUserConnection.h Wed Dec 02 09:52:56 2015 +0100 @@ -164,5 +164,8 @@ void SetTimeout(uint32_t seconds); void DisableTimeout(); + + void FindWorklist(DicomFindAnswers& result, + ParsedDicomFile& query); }; }
--- a/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/IApplicationEntityFilter.h Wed Dec 02 09:52:56 2015 +0100 @@ -38,22 +38,29 @@ namespace Orthanc { - class IApplicationEntityFilter + class IApplicationEntityFilter : public boost::noncopyable { public: virtual ~IApplicationEntityFilter() { } - virtual bool IsAllowedConnection(const std::string& callingIp, - const std::string& callingAet) = 0; + virtual bool IsAllowedConnection(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; - virtual bool IsAllowedRequest(const std::string& callingIp, - const std::string& callingAet, + virtual bool IsAllowedRequest(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, DicomRequestType type) = 0; - virtual bool IsAllowedTransferSyntax(const std::string& callingIp, - const std::string& callingAet, + virtual bool IsAllowedTransferSyntax(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, TransferSyntax syntax) = 0; + + virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; }; }
--- a/OrthancServer/DicomProtocol/IFindRequestHandler.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/IFindRequestHandler.h Wed Dec 02 09:52:56 2015 +0100 @@ -34,28 +34,19 @@ #include "DicomFindAnswers.h" -#include <vector> -#include <string> - - namespace Orthanc { - class IFindRequestHandler + class IFindRequestHandler : public boost::noncopyable { public: virtual ~IFindRequestHandler() { } - /** - * Can throw exceptions. Returns "false" iff too many results have - * to be returned. In such a case, a "Matching terminated due to - * Cancel request" DIMSE code would be returned. - * https://www.dabsoft.ch/dicom/4/V.4.1/ - **/ - virtual bool Handle(DicomFindAnswers& answers, + virtual void Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) = 0; + const std::string& remoteAet, + const std::string& calledAet) = 0; }; }
--- a/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/IFindRequestHandlerFactory.h Wed Dec 02 09:52:56 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IFindRequestHandlerFactory + class IFindRequestHandlerFactory : public boost::noncopyable { public: virtual ~IFindRequestHandlerFactory()
--- a/OrthancServer/DicomProtocol/IMoveRequestHandler.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandler.h Wed Dec 02 09:52:56 2015 +0100 @@ -40,7 +40,7 @@ namespace Orthanc { - class IMoveRequestIterator + class IMoveRequestIterator : public boost::noncopyable { public: enum Status @@ -70,7 +70,8 @@ virtual IMoveRequestIterator* Handle(const std::string& target, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) = 0; + const std::string& remoteAet, + const std::string& calledAet) = 0; }; }
--- a/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/IMoveRequestHandlerFactory.h Wed Dec 02 09:52:56 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IMoveRequestHandlerFactory + class IMoveRequestHandlerFactory : public boost::noncopyable { public: virtual ~IMoveRequestHandlerFactory()
--- a/OrthancServer/DicomProtocol/IStoreRequestHandler.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandler.h Wed Dec 02 09:52:56 2015 +0100 @@ -40,7 +40,7 @@ namespace Orthanc { - class IStoreRequestHandler + class IStoreRequestHandler : public boost::noncopyable { public: virtual ~IStoreRequestHandler()
--- a/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/DicomProtocol/IStoreRequestHandlerFactory.h Wed Dec 02 09:52:56 2015 +0100 @@ -36,7 +36,7 @@ namespace Orthanc { - class IStoreRequestHandlerFactory + class IStoreRequestHandlerFactory : public boost::noncopyable { public: virtual ~IStoreRequestHandlerFactory()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomProtocol/IWorklistRequestHandler.h Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,52 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "DicomFindAnswers.h" + +namespace Orthanc +{ + class IWorklistRequestHandler : public boost::noncopyable + { + public: + virtual ~IWorklistRequestHandler() + { + } + + virtual void Handle(DicomFindAnswers& answers, + ParsedDicomFile& query, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) = 0; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,48 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "IWorklistRequestHandler.h" + +namespace Orthanc +{ + class IWorklistRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IWorklistRequestHandlerFactory() + { + } + + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() = 0; + }; +}
--- a/OrthancServer/FromDcmtkBridge.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/FromDcmtkBridge.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -354,19 +354,6 @@ } - bool FromDcmtkBridge::IsPrivateTag(const DicomTag& tag) - { -#if 1 - DcmTagKey tmp(tag.GetGroup(), tag.GetElement()); - return tmp.isPrivate(); -#else - // Implementation for Orthanc versions <= 0.8.5 - DcmTag tmp(tag.GetGroup(), tag.GetElement()); - return IsPrivateTag(tmp); -#endif - } - - DicomValue* FromDcmtkBridge::ConvertLeafElement(DcmElement& element, DicomToJsonFlags flags, Encoding encoding) @@ -377,26 +364,28 @@ throw OrthancException(ErrorCode_BadParameterType); } - if (element.isaString()) + char *c = NULL; + if (element.isaString() && + element.getString(c).good()) { - char *c; - if (element.getString(c).good()) + if (c == NULL) // This case corresponds to the empty string + { + return new DicomValue("", false); + } + else { - if (c == NULL) // This case corresponds to the empty string + std::string s(c); + std::string utf8 = Toolbox::ConvertToUtf8(s, encoding); + + if (utf8.size() > ORTHANC_MAXIMUM_TAG_LENGTH) { - return new DicomValue("", false); + return new DicomValue; // Create a NULL value } else { - std::string s(c); - std::string utf8 = Toolbox::ConvertToUtf8(s, encoding); return new DicomValue(utf8, false); } } - else - { - return new DicomValue; - } } try @@ -414,24 +403,6 @@ case EVR_OW: // other word case EVR_UN: // unknown value representation case EVR_ox: // OB or OW depending on context - { - if (!(flags & DicomToJsonFlags_ConvertBinaryToNull)) - { - Uint8* data = NULL; - if (element.getUint8Array(data) == EC_Normal) - { - return new DicomValue(reinterpret_cast<const char*>(data), element.getLength(), true); - } - } - - return new DicomValue; - } - - /** - * String types, should never happen at this point because of - * "element.isaString()". - **/ - case EVR_DS: // decimal string case EVR_IS: // integer string case EVR_AS: // age string @@ -447,12 +418,24 @@ case EVR_UT: // unlimited text case EVR_PN: // person name case EVR_UI: // unique identifier - return new DicomValue; - + case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) + case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR + { + if (!(flags & DicomToJsonFlags_ConvertBinaryToNull)) + { + Uint8* data = NULL; + if (element.getUint8Array(data) == EC_Normal) + { + return new DicomValue(reinterpret_cast<const char*>(data), element.getLength(), true); + } + } - /** - * Numberic types - **/ + return new DicomValue; + } + + /** + * Numberic types + **/ case EVR_SL: // signed long { @@ -553,10 +536,8 @@ case EVR_dirRecord: // used internally for DICOMDIR records case EVR_pixelSQ: // used internally for pixel sequences in a compressed image case EVR_pixelItem: // used internally for pixel items in a compressed image - case EVR_UNKNOWN: // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR) case EVR_PixelData: // used internally for uncompressed pixeld data case EVR_OverlayData: // used internally for overlay data - case EVR_UNKNOWN2B: // used internally for elements with unknown VR with 2-byte length field in explicit VR return new DicomValue; @@ -782,8 +763,11 @@ throw OrthancException(ErrorCode_InternalError); } - if (!(flags & DicomToJsonFlags_IncludePrivateTags) && - element->getTag().isPrivate()) + DicomTag tag(FromDcmtkBridge::Convert(element->getTag())); + + /*element->getTag().isPrivate()*/ + if (tag.IsPrivate() && + !(flags & DicomToJsonFlags_IncludePrivateTags)) { continue; } @@ -805,8 +789,6 @@ evr == EVR_ox) { // This is a binary tag - DicomTag tag(FromDcmtkBridge::Convert(element->getTag())); - if ((tag == DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludePixelData)) || (tag != DICOM_TAG_PIXEL_DATA && !(flags & DicomToJsonFlags_IncludeBinary))) { @@ -830,6 +812,17 @@ } + void FromDcmtkBridge::ToJson(Json::Value& target, + DcmMetaInfo& dataset, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength) + { + target = Json::objectValue; + DatasetToJson(target, dataset, format, flags, maxStringLength, Encoding_Ascii); + } + + std::string FromDcmtkBridge::GetName(const DicomTag& t) { // Some patches for important tags because of different DICOM @@ -1073,6 +1066,9 @@ case EVR_TM: return ValueRepresentation_Time; + case EVR_SQ: + return ValueRepresentation_Sequence; + default: return ValueRepresentation_Other; } @@ -1081,8 +1077,7 @@ static bool IsBinaryTag(const DcmTag& key) { - return (key.isPrivate() || - key.isUnknownVR() || + return (key.isUnknownVR() || key.getEVR() == EVR_OB || key.getEVR() == EVR_OF || key.getEVR() == EVR_OW || @@ -1095,7 +1090,8 @@ { DcmTag key(tag.GetGroup(), tag.GetElement()); - if (IsBinaryTag(key)) + if (tag.IsPrivate() || + IsBinaryTag(key)) { return new DcmOtherByteOtherWord(key); } @@ -1238,13 +1234,13 @@ void FromDcmtkBridge::FillElementWithString(DcmElement& element, const DicomTag& tag, const std::string& utf8Value, - bool decodeBinaryTags, + bool decodeDataUriScheme, Encoding dicomEncoding) { std::string binary; const std::string* decoded = &utf8Value; - if (decodeBinaryTags && + if (decodeDataUriScheme && boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) { std::string mime; @@ -1259,7 +1255,8 @@ DcmTag key(tag.GetGroup(), tag.GetElement()); - if (IsBinaryTag(key)) + if (tag.IsPrivate() || + IsBinaryTag(key)) { if (element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good()) { @@ -1409,7 +1406,7 @@ DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag, const Json::Value& value, - bool decodeBinaryTags, + bool decodeDataUriScheme, Encoding dicomEncoding) { std::auto_ptr<DcmElement> element; @@ -1418,7 +1415,7 @@ { case Json::stringValue: element.reset(CreateElementForTag(tag)); - FillElementWithString(*element, tag, value.asString(), decodeBinaryTags, dicomEncoding); + FillElementWithString(*element, tag, value.asString(), decodeDataUriScheme, dicomEncoding); break; case Json::arrayValue: @@ -1439,7 +1436,7 @@ Json::Value::Members members = value[i].getMemberNames(); for (Json::Value::ArrayIndex j = 0; j < members.size(); j++) { - item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeBinaryTags, dicomEncoding)); + item->insert(FromJson(ParseTag(members[j]), value[i][members[j]], decodeDataUriScheme, dicomEncoding)); } sequence->append(item.release());
--- a/OrthancServer/FromDcmtkBridge.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/FromDcmtkBridge.h Wed Dec 02 09:52:56 2015 +0100 @@ -37,6 +37,7 @@ #include "../Core/DicomFormat/DicomMap.h" #include <dcmtk/dcmdata/dcdatset.h> +#include <dcmtk/dcmdata/dcmetinf.h> #include <json/json.h> namespace Orthanc @@ -60,8 +61,6 @@ static DicomTag GetTag(const DcmElement& element); - static bool IsPrivateTag(const DicomTag& tag); - static bool IsUnknownTag(const DicomTag& tag); static DicomValue* ConvertLeafElement(DcmElement& element, @@ -81,6 +80,12 @@ DicomToJsonFlags flags, unsigned int maxStringLength); + static void ToJson(Json::Value& target, + DcmMetaInfo& header, + DicomToJsonFormat format, + DicomToJsonFlags flags, + unsigned int maxStringLength); + static std::string GetName(const DicomTag& tag); static DicomTag ParseTag(const char* name); @@ -125,12 +130,12 @@ static void FillElementWithString(DcmElement& element, const DicomTag& tag, const std::string& utf8alue, // Encoded using UTF-8 - bool interpretBinaryTags, + bool decodeDataUriScheme, Encoding dicomEncoding); static DcmElement* FromJson(const DicomTag& tag, const Json::Value& element, // Encoding using UTF-8 - bool interpretBinaryTags, + bool decodeDataUriScheme, Encoding dicomEncoding); static DcmEVR ParseValueRepresentation(const std::string& s);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/IDicomImageDecoder.h Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../Core/Images/ImageAccessor.h" + +#include <boost/noncopyable.hpp> + +namespace Orthanc +{ + class ParsedDicomFile; + + class IDicomImageDecoder : public boost::noncopyable + { + public: + virtual ~IDicomImageDecoder() + { + } + + virtual ImageAccessor* Decode(ParsedDicomFile& dicom, + unsigned int frame) = 0; + }; +}
--- a/OrthancServer/Internals/CommandDispatcher.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/CommandDispatcher.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -91,15 +91,11 @@ #include <dcmtk/dcmnet/dcasccfg.h> /* for class DcmAssociationConfiguration */ #include <boost/lexical_cast.hpp> -#define ORTHANC_PROMISCUOUS 1 - static OFBool opt_rejectWithoutImplementationUID = OFFalse; -#if ORTHANC_PROMISCUOUS == 1 -static -DUL_PRESENTATIONCONTEXT * +static DUL_PRESENTATIONCONTEXT * findPresentationContextID(LST_HEAD * head, T_ASC_PresentationContextID presentationContextID) { @@ -231,7 +227,6 @@ } return cond; } -#endif @@ -430,6 +425,11 @@ knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel); } + if (server.HasWorklistRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel); + } + // For C-MOVE if (server.HasMoveRequestHandlerFactory()) { @@ -460,17 +460,17 @@ } // Retrieve the AET and the IP address of the remote modality - std::string callingAet; - std::string callingIp; + std::string remoteAet; + std::string remoteIp; std::string calledAet; { - DIC_AE callingAet_C; + DIC_AE remoteAet_C; DIC_AE calledAet_C; - DIC_AE callingIp_C; + DIC_AE remoteIp_C; DIC_AE calledIP_C; - if (ASC_getAPTitles(assoc->params, callingAet_C, calledAet_C, NULL).bad() || - ASC_getPresentationAddresses(assoc->params, callingIp_C, calledIP_C).bad()) + if (ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() || + ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()) { T_ASC_RejectParameters rej = { @@ -483,13 +483,13 @@ return NULL; } - callingIp = std::string(/*OFSTRING_GUARD*/(callingIp_C)); - callingAet = std::string(/*OFSTRING_GUARD*/(callingAet_C)); + remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C)); + remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C)); calledAet = (/*OFSTRING_GUARD*/(calledAet_C)); } - LOG(INFO) << "Association Received from AET " << callingAet - << " on IP " << callingIp; + LOG(INFO) << "Association Received from AET " << remoteAet + << " on IP " << remoteIp; std::vector<const char*> transferSyntaxes; @@ -501,13 +501,13 @@ // New transfer syntaxes supported since Orthanc 0.7.2 if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Deflated)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated)) { transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg)) { transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax); transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax); @@ -532,14 +532,14 @@ } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpeg2000)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000)) { transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_JpegLossless)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless)) { transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax); transferSyntaxes.push_back(UID_JPEG2000TransferSyntax); @@ -548,21 +548,21 @@ } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Jpip)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip)) { transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax); transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Mpeg2)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2)) { transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax); transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax); } if (!server.HasApplicationEntityFilter() || - server.GetApplicationEntityFilter().IsAllowedTransferSyntax(callingIp, callingAet, TransferSyntax_Rle)) + server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle)) { transferSyntaxes.push_back(UID_RLELosslessTransferSyntax); } @@ -585,17 +585,22 @@ return NULL; } -#if ORTHANC_PROMISCUOUS == 1 - /* accept everything not known not to be a storage SOP class */ - cond = acceptUnknownContextsWithPreferredTransferSyntaxes( - assoc->params, &transferSyntaxes[0], transferSyntaxes.size()); - if (cond.bad()) + if (!server.HasApplicationEntityFilter() || + server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) { - LOG(INFO) << cond.text(); - AssociationCleanup(assoc); - return NULL; + /* + * Promiscous mode is enabled: Accept everything not known not + * to be a storage SOP class. + **/ + cond = acceptUnknownContextsWithPreferredTransferSyntaxes( + assoc->params, &transferSyntaxes[0], transferSyntaxes.size()); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } } -#endif /* set our app title */ ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str()); @@ -638,9 +643,9 @@ } if (server.HasApplicationEntityFilter() && - !server.GetApplicationEntityFilter().IsAllowedConnection(callingIp, callingAet)) + !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet)) { - LOG(WARNING) << "Rejected association for remote AET " << callingAet << " on IP " << callingIp; + LOG(WARNING) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp; T_ASC_RejectParameters rej = { ASC_RESULT_REJECTEDPERMANENT, @@ -687,7 +692,7 @@ } IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL; - return new CommandDispatcher(server, assoc, callingIp, callingAet, filter); + return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, filter); } bool CommandDispatcher::Step() @@ -771,7 +776,7 @@ if (supported && request != DicomRequestType_Echo && // Always allow incoming ECHO requests filter_ != NULL && - !filter_->IsAllowedRequest(remoteIp_, remoteAet_, request)) + !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request)) { LOG(ERROR) << EnumerationToString(request) << " requests are disallowed for the AET \"" @@ -798,7 +803,11 @@ { std::auto_ptr<IStoreRequestHandler> handler (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler()); - cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_); + + if (handler.get() != NULL) + { + cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_); + } } break; @@ -807,16 +816,32 @@ { std::auto_ptr<IMoveRequestHandler> handler (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler()); - cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_); + + if (handler.get() != NULL) + { + cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_); + } } break; case DicomRequestType_Find: - if (server_.HasFindRequestHandlerFactory()) // Should always be true + if (server_.HasFindRequestHandlerFactory() || // Should always be true + server_.HasWorklistRequestHandlerFactory()) { - std::auto_ptr<IFindRequestHandler> handler - (server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler()); - cond = Internals::findScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_); + std::auto_ptr<IFindRequestHandler> findHandler; + if (server_.HasFindRequestHandlerFactory()) + { + findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler()); + } + + std::auto_ptr<IWorklistRequestHandler> worklistHandler; + if (server_.HasWorklistRequestHandlerFactory()) + { + worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler()); + } + + cond = Internals::findScp(assoc_, &msg, presID, findHandler.get(), + worklistHandler.get(), remoteIp_, remoteAet_, calledAet_); } break;
--- a/OrthancServer/Internals/CommandDispatcher.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/CommandDispatcher.h Wed Dec 02 09:52:56 2015 +0100 @@ -52,6 +52,7 @@ T_ASC_Association* assoc_; std::string remoteIp_; std::string remoteAet_; + std::string calledAet_; IApplicationEntityFilter* filter_; public: @@ -59,11 +60,13 @@ T_ASC_Association* assoc, const std::string& remoteIp, const std::string& remoteAet, + const std::string& calledAet, IApplicationEntityFilter* filter) : server_(server), assoc_(assoc), remoteIp_(remoteIp), remoteAet_(remoteAet), + calledAet_(calledAet), filter_(filter) { clientTimeout_ = server.GetClientTimeout();
--- a/OrthancServer/Internals/DicomImageDecoder.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/DicomImageDecoder.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -82,14 +82,17 @@ #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" +#include "../../Core/Images/Image.h" #include "../../Core/Images/ImageProcessing.h" -#include "../../Core/Images/PngWriter.h" // TODO REMOVE THIS #include "../../Core/DicomFormat/DicomIntegerPixelAccessor.h" #include "../ToDcmtkBridge.h" #include "../FromDcmtkBridge.h" +#include "../ParsedDicomFile.h" #include <boost/lexical_cast.hpp> +#include <dcmtk/dcmdata/dcfilefo.h> + #if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 #include <dcmtk/dcmjpls/djcodecd.h> #include <dcmtk/dcmjpls/djcparam.h> @@ -303,8 +306,7 @@ }; - void DicomImageDecoder::SetupImageBuffer(ImageBuffer& target, - DcmDataset& dataset) + ImageAccessor* DicomImageDecoder::CreateImage(DcmDataset& dataset) { DicomMap m; FromDcmtkBridge::Convert(m, dataset); @@ -323,9 +325,7 @@ throw OrthancException(ErrorCode_NotImplemented); } - target.SetHeight(info.GetHeight()); - target.SetWidth(info.GetWidth()); - target.SetFormat(format); + return new Image(format, info.GetWidth(), info.GetHeight()); } @@ -373,22 +373,20 @@ } - void DicomImageDecoder::DecodeUncompressedImage(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame) + ImageAccessor* DicomImageDecoder::DecodeUncompressedImage(DcmDataset& dataset, + unsigned int frame) { if (!IsUncompressedImage(dataset)) { throw OrthancException(ErrorCode_BadParameterType); } - DecodeUncompressedImageInternal(target, dataset, frame); + return DecodeUncompressedImageInternal(dataset, frame); } - void DicomImageDecoder::DecodeUncompressedImageInternal(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame) + ImageAccessor* DicomImageDecoder::DecodeUncompressedImageInternal(DcmDataset& dataset, + unsigned int frame) { ImageSource source; source.Setup(dataset, frame); @@ -398,10 +396,10 @@ * Resize the target image. **/ - SetupImageBuffer(target, dataset); + std::auto_ptr<ImageAccessor> target(CreateImage(dataset)); - if (source.GetWidth() != target.GetWidth() || - source.GetHeight() != target.GetHeight()) + if (source.GetWidth() != target->GetWidth() || + source.GetHeight() != target->GetHeight()) { throw OrthancException(ErrorCode_InternalError); } @@ -412,7 +410,6 @@ * direct access to copy its values. **/ - ImageAccessor targetAccessor(target.GetAccessor()); const DicomImageInformation& info = source.GetAccessor().GetInformation(); bool fastVersionSuccess = false; @@ -434,8 +431,8 @@ info.GetWidth() * GetBytesPerPixel(sourceFormat), buffer + frame * frameSize); - ImageProcessing::Convert(targetAccessor, sourceImage); - ImageProcessing::ShiftRight(targetAccessor, info.GetShift()); + ImageProcessing::Convert(*target, sourceImage); + ImageProcessing::ShiftRight(*target, info.GetShift()); fastVersionSuccess = true; } } @@ -452,33 +449,34 @@ if (!fastVersionSuccess) { - switch (target.GetFormat()) + switch (target->GetFormat()) { case PixelFormat_RGB24: case PixelFormat_RGBA32: case PixelFormat_Grayscale8: - CopyPixels<uint8_t>(targetAccessor, source.GetAccessor()); + CopyPixels<uint8_t>(*target, source.GetAccessor()); break; case PixelFormat_Grayscale16: - CopyPixels<uint16_t>(targetAccessor, source.GetAccessor()); + CopyPixels<uint16_t>(*target, source.GetAccessor()); break; case PixelFormat_SignedGrayscale16: - CopyPixels<int16_t>(targetAccessor, source.GetAccessor()); + CopyPixels<int16_t>(*target, source.GetAccessor()); break; default: throw OrthancException(ErrorCode_InternalError); } } + + return target.release(); } #if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 - void DicomImageDecoder::DecodeJpegLossless(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame) + ImageAccessor* DicomImageDecoder::DecodeJpegLossless(DcmDataset& dataset, + unsigned int frame) { if (!IsJpegLossless(dataset)) { @@ -499,9 +497,7 @@ throw OrthancException(ErrorCode_BadFileFormat); } - SetupImageBuffer(target, dataset); - - ImageAccessor targetAccessor(target.GetAccessor()); + std::auto_ptr<ImageAccessor> target(CreateImage(dataset)); /** * The "DJLSLosslessDecoder" and "DJLSNearLosslessDecoder" in DCMTK @@ -517,36 +513,36 @@ OFString decompressedColorModel; // Out DJ_RPLossless representationParameter; OFCondition c = decoder.decodeFrame(&representationParameter, pixelSequence, ¶meters, - &dataset, frame, startFragment, targetAccessor.GetBuffer(), - targetAccessor.GetSize(), decompressedColorModel); + &dataset, frame, startFragment, target->GetBuffer(), + target->GetSize(), decompressedColorModel); if (!c.good()) { throw OrthancException(ErrorCode_InternalError); } + + return target.release(); } #endif - bool DicomImageDecoder::Decode(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame) + ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom, + unsigned int frame) { + DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); + if (IsUncompressedImage(dataset)) { - DecodeUncompressedImage(target, dataset, frame); - return true; + return DecodeUncompressedImage(dataset, frame); } - #if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 if (IsJpegLossless(dataset)) { LOG(INFO) << "Decoding a JPEG-LS image"; - DecodeJpegLossless(target, dataset, frame); - return true; + return DecodeJpegLossless(dataset, frame); } #endif @@ -555,7 +551,6 @@ // TODO Implement this part to speed up JPEG decompression #endif - /** * This DICOM image format is not natively supported by * Orthanc. As a last resort, try and decode it through @@ -572,12 +567,11 @@ if (converted->canWriteXfer(EXS_LittleEndianExplicit)) { - DecodeUncompressedImageInternal(target, *converted, frame); - return true; + return DecodeUncompressedImageInternal(*converted, frame); } } - return false; + return NULL; } @@ -588,23 +582,13 @@ } - bool DicomImageDecoder::DecodeAndTruncate(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame, - PixelFormat format, - bool allowColorConversion) + bool DicomImageDecoder::TruncateDecodedImage(std::auto_ptr<ImageAccessor>& image, + PixelFormat format, + bool allowColorConversion) { - // TODO Special case for uncompressed images - - ImageBuffer source; - if (!Decode(source, dataset, frame)) - { - return false; - } - // If specified, prevent the conversion between color and // grayscale images - bool isSourceColor = IsColorImage(source.GetFormat()); + bool isSourceColor = IsColorImage(image->GetFormat()); bool isTargetColor = IsColorImage(format); if (!allowColorConversion) @@ -615,43 +599,25 @@ } } - if (source.GetFormat() == format) + if (image->GetFormat() != format) { - // No conversion is required, return the temporary image - target.AcquireOwnership(source); - return true; + // A conversion is required + std::auto_ptr<ImageAccessor> target(new Image(format, image->GetWidth(), image->GetHeight())); + ImageProcessing::Convert(*target, *image); + image = target; } - target.SetFormat(format); - target.SetWidth(source.GetWidth()); - target.SetHeight(source.GetHeight()); - - ImageAccessor targetAccessor(target.GetAccessor()); - ImageAccessor sourceAccessor(source.GetAccessor()); - ImageProcessing::Convert(targetAccessor, sourceAccessor); - return true; } - bool DicomImageDecoder::DecodePreview(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame) + bool DicomImageDecoder::PreviewDecodedImage(std::auto_ptr<ImageAccessor>& image) { - // TODO Special case for uncompressed images - - ImageBuffer source; - if (!Decode(source, dataset, frame)) - { - return false; - } - - switch (source.GetFormat()) + switch (image->GetFormat()) { case PixelFormat_RGB24: { - // Directly return color images (RGB) - target.AcquireOwnership(source); + // Directly return color images without modification (RGB) return true; } @@ -660,32 +626,24 @@ case PixelFormat_SignedGrayscale16: { // Grayscale image: Stretch its dynamics to the [0,255] range - target.SetFormat(PixelFormat_Grayscale8); - target.SetWidth(source.GetWidth()); - target.SetHeight(source.GetHeight()); + int64_t a, b; + ImageProcessing::GetMinMaxValue(a, b, *image); - ImageAccessor targetAccessor(target.GetAccessor()); - ImageAccessor sourceAccessor(source.GetAccessor()); - - int64_t a, b; - ImageProcessing::GetMinMaxValue(a, b, sourceAccessor); - if (a == b) { - ImageProcessing::Set(targetAccessor, 0); + ImageProcessing::Set(*image, 0); } else { - ImageProcessing::ShiftScale(sourceAccessor, static_cast<float>(-a), 255.0f / static_cast<float>(b - a)); + ImageProcessing::ShiftScale(*image, static_cast<float>(-a), 255.0f / static_cast<float>(b - a)); + } - if (source.GetFormat() == PixelFormat_Grayscale8) - { - target.AcquireOwnership(source); - } - else - { - ImageProcessing::Convert(targetAccessor, sourceAccessor); - } + // If the source image is not grayscale 8bpp, convert it + if (image->GetFormat() != PixelFormat_Grayscale8) + { + std::auto_ptr<ImageAccessor> target(new Image(PixelFormat_Grayscale8, image->GetWidth(), image->GetHeight())); + ImageProcessing::Convert(*target, *image); + image = target; } return true;
--- a/OrthancServer/Internals/DicomImageDecoder.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/DicomImageDecoder.h Wed Dec 02 09:52:56 2015 +0100 @@ -32,51 +32,44 @@ #pragma once -#include <dcmtk/dcmdata/dcfilefo.h> +#include <memory> -#include "../../Core/Images/ImageBuffer.h" +#include "../IDicomImageDecoder.h" + +class DcmDataset; namespace Orthanc { - class DicomImageDecoder + class DicomImageDecoder : public IDicomImageDecoder { private: class ImageSource; - static void DecodeUncompressedImageInternal(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame); + static ImageAccessor* DecodeUncompressedImageInternal(DcmDataset& dataset, + unsigned int frame); static bool IsPsmctRle1(DcmDataset& dataset); - static void SetupImageBuffer(ImageBuffer& target, - DcmDataset& dataset); + static ImageAccessor* CreateImage(DcmDataset& dataset); static bool IsUncompressedImage(const DcmDataset& dataset); - static void DecodeUncompressedImage(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame); + static ImageAccessor* DecodeUncompressedImage(DcmDataset& dataset, + unsigned int frame); #if ORTHANC_JPEG_LOSSLESS_ENABLED == 1 - static void DecodeJpegLossless(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame); + static ImageAccessor* DecodeJpegLossless(DcmDataset& dataset, + unsigned int frame); #endif public: - static bool Decode(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame); + virtual ImageAccessor *Decode(ParsedDicomFile& dicom, + unsigned int frame); - static bool DecodeAndTruncate(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame, - PixelFormat format, - bool allowColorConversion); + static bool TruncateDecodedImage(std::auto_ptr<ImageAccessor>& image, + PixelFormat format, + bool allowColorConversion); - static bool DecodePreview(ImageBuffer& target, - DcmDataset& dataset, - unsigned int frame); + static bool PreviewDecodedImage(std::auto_ptr<ImageAccessor>& image); }; }
--- a/OrthancServer/Internals/FindScp.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/FindScp.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -87,7 +87,7 @@ #include "../../Core/Logging.h" #include "../../Core/OrthancException.h" - +#include <dcmtk/dcmdata/dcfilefo.h> namespace Orthanc { @@ -95,13 +95,13 @@ { struct FindScpData { - IFindRequestHandler* handler_; - DicomMap input_; + IFindRequestHandler* findHandler_; + IWorklistRequestHandler* worklistHandler_; DicomFindAnswers answers_; DcmDataset* lastRequest_; const std::string* remoteIp_; const std::string* remoteAet_; - bool noCroppingOfResults_; + const std::string* calledAet_; }; @@ -120,20 +120,55 @@ bzero(response, sizeof(T_DIMSE_C_FindRSP)); *statusDetail = NULL; + std::string sopClassUid(request->AffectedSOPClassUID); + FindScpData& data = *reinterpret_cast<FindScpData*>(callbackData); if (data.lastRequest_ == NULL) { - FromDcmtkBridge::Convert(data.input_, *requestIdentifiers); + bool ok = false; try { - data.noCroppingOfResults_ = data.handler_->Handle(data.answers_, data.input_, - *data.remoteIp_, *data.remoteAet_); + if (sopClassUid == UID_FINDModalityWorklistInformationModel) + { + if (data.worklistHandler_ != NULL) + { + ParsedDicomFile query(*requestIdentifiers); + data.worklistHandler_->Handle(data.answers_, query, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_); + ok = true; + } + else + { + LOG(ERROR) << "No worklist handler is installed, cannot handle this C-FIND request"; + } + } + else + { + if (data.findHandler_ != NULL) + { + DicomMap input; + FromDcmtkBridge::Convert(input, *requestIdentifiers); + data.findHandler_->Handle(data.answers_, input, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_); + ok = true; + } + else + { + LOG(ERROR) << "No C-Find handler is installed, cannot handle this request"; + } + } } catch (OrthancException& e) { // Internal error! LOG(ERROR) << "C-FIND request handler has failed: " << e.What(); + } + + if (!ok) + { response->DimseStatus = STATUS_FIND_Failed_UnableToProcess; *responseIdentifiers = NULL; return; @@ -153,9 +188,9 @@ { // There are pending results that are still to be sent response->DimseStatus = STATUS_Pending; - *responseIdentifiers = ToDcmtkBridge::Convert(data.answers_.GetAnswer(responseCount - 1)); + *responseIdentifiers = data.answers_.ExtractDcmDataset(responseCount - 1); } - else if (data.noCroppingOfResults_) + else if (data.answers_.IsComplete()) { // Success: All the results have been sent response->DimseStatus = STATUS_Success; @@ -175,16 +210,19 @@ OFCondition Internals::findScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID, - IFindRequestHandler& handler, + IFindRequestHandler* findHandler, + IWorklistRequestHandler* worklistHandler, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { FindScpData data; data.lastRequest_ = NULL; - data.handler_ = &handler; + data.findHandler_ = findHandler; + data.worklistHandler_ = worklistHandler; data.remoteIp_ = &remoteIp; data.remoteAet_ = &remoteAet; - data.noCroppingOfResults_ = true; + data.calledAet_ = &calledAet; OFCondition cond = DIMSE_findProvider(assoc, presID, &msg->msg.CFindRQ, FindScpCallback, &data,
--- a/OrthancServer/Internals/FindScp.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/FindScp.h Wed Dec 02 09:52:56 2015 +0100 @@ -33,6 +33,7 @@ #pragma once #include "../DicomProtocol/IFindRequestHandler.h" +#include "../DicomProtocol/IWorklistRequestHandler.h" #include <dcmtk/dcmnet/dimse.h> @@ -43,8 +44,10 @@ OFCondition findScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID, - IFindRequestHandler& handler, + IFindRequestHandler* findHandler, // can be NULL + IWorklistRequestHandler* worklistHandler, // can be NULL const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); } }
--- a/OrthancServer/Internals/MoveScp.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/MoveScp.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -98,7 +98,6 @@ { std::string target_; IMoveRequestHandler* handler_; - DicomMap input_; DcmDataset* lastRequest_; unsigned int subOperationCount_; unsigned int failureCount_; @@ -106,6 +105,7 @@ std::auto_ptr<IMoveRequestIterator> iterator_; const std::string* remoteIp_; const std::string* remoteAet_; + const std::string* calledAet_; }; @@ -128,12 +128,14 @@ MoveScpData& data = *reinterpret_cast<MoveScpData*>(callbackData); if (data.lastRequest_ == NULL) { - FromDcmtkBridge::Convert(data.input_, *requestIdentifiers); + DicomMap input; + FromDcmtkBridge::Convert(input, *requestIdentifiers); try { - data.iterator_.reset(data.handler_->Handle(data.target_, data.input_, - *data.remoteIp_, *data.remoteAet_)); + data.iterator_.reset(data.handler_->Handle(data.target_, input, + *data.remoteIp_, *data.remoteAet_, + *data.calledAet_)); if (data.iterator_.get() == NULL) { @@ -215,7 +217,8 @@ T_ASC_PresentationContextID presID, IMoveRequestHandler& handler, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { MoveScpData data; data.target_ = std::string(msg->msg.CMoveRQ.MoveDestination); @@ -223,6 +226,7 @@ data.handler_ = &handler; data.remoteIp_ = &remoteIp; data.remoteAet_ = &remoteAet; + data.calledAet_ = &calledAet; OFCondition cond = DIMSE_moveProvider(assoc, presID, &msg->msg.CMoveRQ, MoveScpCallback, &data,
--- a/OrthancServer/Internals/MoveScp.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/MoveScp.h Wed Dec 02 09:52:56 2015 +0100 @@ -45,6 +45,7 @@ T_ASC_PresentationContextID presID, IMoveRequestHandler& handler, const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); } }
--- a/OrthancServer/Internals/StoreScp.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Internals/StoreScp.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -171,7 +171,7 @@ FromDcmtkBridge::ToJson(dicomJson, **imageDataSet, DicomToJsonFormat_Full, DicomToJsonFlags_Default, - 256 /* max string length */); + ORTHANC_MAXIMUM_TAG_LENGTH); if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer, **imageDataSet)) {
--- a/OrthancServer/OrthancFindRequestHandler.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -88,13 +88,50 @@ } - bool OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, + + bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */, + ResourceType level, + const DicomTag& tag, + ModalityManufacturer manufacturer) + { + switch (manufacturer) + { + case ModalityManufacturer_EFilm2: + // Following Denis Nesterov's mail on 2015-11-30 + if (tag == DicomTag(0x0008, 0x0000) || // "GenericGroupLength" + tag == DicomTag(0x0010, 0x0000) || // "GenericGroupLength" + tag == DicomTag(0x0020, 0x0000)) // "GenericGroupLength" + { + return false; + } + + break; + + case ModalityManufacturer_Vitrea: + // Following Denis Nesterov's mail on 2015-11-30 + if (tag == DicomTag(0x5653, 0x0010)) // "PrivateCreator = Vital Images SW 3.4" + { + return false; + } + + break; + + default: + break; + } + + return true; + } + + + void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { /** - * Ensure that the calling modality is known to Orthanc. + * Ensure that the remote modality is known to Orthanc. **/ RemoteModalityParameters modality; @@ -104,8 +141,6 @@ throw OrthancException(ErrorCode_UnknownModality); } - // ModalityManufacturer manufacturer = modality.GetManufacturer(); - bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false); @@ -118,6 +153,7 @@ levelTmp->IsNull() || levelTmp->IsBinary()) { + LOG(ERROR) << "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)"; throw OrthancException(ErrorCode_BadRequest); } @@ -170,17 +206,25 @@ continue; } - ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + if (FilterQueryTag(value, level, tag, modality.GetManufacturer())) + { + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); - // DICOM specifies that searches must be case sensitive, except - // for tags with a PN value representation - bool sensitive = true; - if (vr == ValueRepresentation_PatientName) + // DICOM specifies that searches must be case sensitive, except + // for tags with a PN value representation + bool sensitive = true; + if (vr == ValueRepresentation_PatientName) + { + sensitive = caseSensitivePN; + } + + finder.AddDicomConstraint(tag, value, sensitive); + } + else { - sensitive = caseSensitivePN; + LOG(INFO) << "Because of a patch for the manufacturer of the remote modality, " + << "ignoring constraint on tag (" << tag.Format() << ") " << FromDcmtkBridge::GetName(tag); } - - finder.AddDicomConstraint(tag, value, sensitive); } @@ -194,7 +238,7 @@ context_.GetIndex().FindCandidates(resources, instances, finder); assert(resources.size() == instances.size()); - bool finished = true; + bool complete = true; for (size_t i = 0; i < instances.size(); i++) { @@ -206,7 +250,7 @@ if (maxResults != 0 && answers.GetSize() >= maxResults) { - finished = false; + complete = false; break; } else @@ -218,6 +262,6 @@ LOG(INFO) << "Number of matching resources: " << answers.GetSize(); - return finished; + answers.SetComplete(complete); } }
--- a/OrthancServer/OrthancFindRequestHandler.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.h Wed Dec 02 09:52:56 2015 +0100 @@ -47,6 +47,11 @@ bool HasReachedLimit(const DicomFindAnswers& answers, ResourceType level) const; + bool FilterQueryTag(std::string& value /* can be modified */, + ResourceType level, + const DicomTag& tag, + ModalityManufacturer manufacturer); + public: OrthancFindRequestHandler(ServerContext& context) : context_(context), @@ -55,10 +60,11 @@ { } - virtual bool Handle(DicomFindAnswers& answers, + virtual void Handle(DicomFindAnswers& answers, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); unsigned int GetMaxResults() const {
--- a/OrthancServer/OrthancMoveRequestHandler.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -165,7 +165,8 @@ IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet) + const std::string& remoteAet, + const std::string& calledAet) { LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\"";
--- a/OrthancServer/OrthancMoveRequestHandler.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.h Wed Dec 02 09:52:56 2015 +0100 @@ -54,6 +54,7 @@ virtual IMoveRequestIterator* Handle(const std::string& targetAet, const DicomMap& input, const std::string& remoteIp, - const std::string& remoteAet); + const std::string& remoteAet, + const std::string& calledAet); }; }
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -620,7 +620,7 @@ throw OrthancException(ErrorCode_BadRequest); } - ParsedDicomFile dicom; + ParsedDicomFile dicom(true); { Encoding encoding; @@ -834,7 +834,7 @@ else { // Compatibility with Orthanc <= 0.9.3 - ParsedDicomFile dicom; + ParsedDicomFile dicom(true); CreateDicomV1(dicom, call, request); std::string id;
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -451,8 +451,10 @@ ArchiveWriterVisitor(HierarchicalZipWriter& writer, ServerContext& context) : writer_(writer), - context_(context) + context_(context), + countInstances_(0) { + snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm"); } virtual void Open(ResourceType level,
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -280,6 +280,18 @@ } + static void CopyTagIfExists(DicomMap& target, + ParsedDicomFile& source, + const DicomTag& tag) + { + std::string tmp; + if (source.GetTagValue(tmp, tag)) + { + target.SetValue(tag, tmp); + } + } + + static void DicomFind(RestApiPostCall& call) { LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri(); @@ -303,15 +315,16 @@ Json::Value result = Json::arrayValue; for (size_t i = 0; i < patients.GetSize(); i++) { - Json::Value patient(Json::objectValue); - FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i), true); + Json::Value patient; + patients.ToJson(patient, i, true); DicomMap::SetupFindStudyTemplate(m); if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize())) { return; } - m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); + + CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); DicomFindAnswers studies; FindStudy(studies, locker.GetConnection(), m); @@ -321,16 +334,17 @@ // Loop over the found studies for (size_t j = 0; j < studies.GetSize(); j++) { - Json::Value study(Json::objectValue); - FromDcmtkBridge::ToJson(study, studies.GetAnswer(j), true); + Json::Value study; + studies.ToJson(study, j, true); DicomMap::SetupFindSeriesTemplate(m); if (!MergeQueryAndTemplate(m, call.GetBodyData(), call.GetBodySize())) { return; } - m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); - m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); + + CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); + CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); DicomFindAnswers series; FindSeries(series, locker.GetConnection(), m); @@ -339,8 +353,8 @@ study["Series"] = Json::arrayValue; for (size_t k = 0; k < series.GetSize(); k++) { - Json::Value series2(Json::objectValue); - FromDcmtkBridge::ToJson(series2, series.GetAnswer(k), true); + Json::Value series2; + series.ToJson(series2, k, true); study["Series"].append(series2); } @@ -465,8 +479,13 @@ static void GetQueryOneAnswer(RestApiGetCall& call) { size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", "")); + QueryAccessor query(call); - AnswerDicomMap(call, query->GetAnswer(index), call.HasArgument("simplify")); + + DicomMap map; + query->GetAnswer(map, index); + + AnswerDicomMap(call, map, call.HasArgument("simplify")); } @@ -547,7 +566,9 @@ // Ensure that the answer of interest does exist size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", "")); - query->GetAnswer(index); + + DicomMap map; + query->GetAnswer(map, index); RestApi::AutoListChildren(call); } @@ -844,6 +865,32 @@ } + static void DicomFindWorklist(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value json; + if (call.ParseJsonRequest(json)) + { + const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle(); + RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", "")); + + std::auto_ptr<ParsedDicomFile> query(ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0))); + + DicomFindAnswers answers; + + { + ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote); + locker.GetConnection().FindWorklist(answers, *query); + } + + Json::Value result; + answers.ToJson(result, true); + call.GetOutput().AnswerJson(result); + } + } + + void OrthancRestApi::RegisterModalities() { Register("/modalities", ListModalities); @@ -877,5 +924,7 @@ Register("/peers/{id}", UpdatePeer); Register("/peers/{id}", DeletePeer); Register("/peers/{id}/store", PeerStore); + + Register("/modalities/{id}/find-worklist", DicomFindWorklist); } }
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -39,6 +39,7 @@ #include "../FromDcmtkBridge.h" #include "../ServerContext.h" #include "../SliceOrdering.h" +#include "../Internals/DicomImageDecoder.h" namespace Orthanc @@ -265,6 +266,7 @@ class ImageToEncode { private: + IDicomImageDecoder& decoder_; std::string format_; std::string encoded_; ParsedDicomFile& dicom_; @@ -272,9 +274,11 @@ ImageExtractionMode mode_; public: - ImageToEncode(ParsedDicomFile& dicom, + ImageToEncode(IDicomImageDecoder& decoder, + ParsedDicomFile& dicom, unsigned int frame, ImageExtractionMode mode) : + decoder_(decoder), dicom_(dicom), frame_(frame), mode_(mode) @@ -310,6 +314,11 @@ { output.AnswerBuffer(encoded_, format_); } + + IDicomImageDecoder& GetDecoder() const + { + return decoder_; + } }; class EncodePng : public HttpContentNegociation::IHandler @@ -327,7 +336,8 @@ { assert(type == "image"); assert(subtype == "png"); - image_.GetDicom().ExtractPngImage(image_.GetTarget(), image_.GetFrame(), image_.GetMode()); + image_.GetDicom().ExtractPngImage(image_.GetTarget(), image_.GetDecoder(), + image_.GetFrame(), image_.GetMode()); image_.SetFormat("image/png"); } }; @@ -349,7 +359,7 @@ try { quality_ = boost::lexical_cast<unsigned int>(v); - ok = (quality_ >= 0 && quality_ <= 100); + ok = (quality_ >= 1 && quality_ <= 100); } catch (boost::bad_lexical_cast&) { @@ -367,7 +377,8 @@ { assert(type == "image"); assert(subtype == "jpeg"); - image_.GetDicom().ExtractJpegImage(image_.GetTarget(), image_.GetFrame(), image_.GetMode(), quality_); + image_.GetDicom().ExtractJpegImage(image_.GetTarget(), image_.GetDecoder(), + image_.GetFrame(), image_.GetMode(), quality_); image_.SetFormat("image/jpeg"); } }; @@ -399,7 +410,13 @@ try { - ImageToEncode image(dicom, frame, mode); +#if ORTHANC_PLUGINS_ENABLED == 1 + IDicomImageDecoder& decoder = context.GetPlugins(); +#else + DicomImageDecoder decoder; // This is Orthanc's built-in decoder +#endif + + ImageToEncode image(decoder, dicom, frame, mode); HttpContentNegociation negociation; EncodePng png(image); negociation.Register("image/png", png); @@ -451,14 +468,17 @@ std::string dicomContent; context.ReadFile(dicomContent, publicId, FileContentType_Dicom); +#if ORTHANC_PLUGINS_ENABLED == 1 + IDicomImageDecoder& decoder = context.GetPlugins(); +#else + DicomImageDecoder decoder; // This is Orthanc's built-in decoder +#endif + ParsedDicomFile dicom(dicomContent); - ImageBuffer buffer; - dicom.ExtractImage(buffer, frame); - - ImageAccessor accessor(buffer.GetConstAccessor()); + std::auto_ptr<ImageAccessor> decoded(dicom.ExtractImage(decoder, frame)); std::string result; - accessor.ToMatlabString(result); + decoded->ToMatlabString(result); call.GetOutput().AnswerBuffer(result, "text/plain"); } @@ -1077,11 +1097,13 @@ size_t limit = 0; if (request.isMember("Limit")) { - limit = request["CaseSensitive"].asInt(); - if (limit < 0) + int tmp = request["CaseSensitive"].asInt(); + if (tmp < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange); } + + limit = static_cast<size_t>(tmp); } std::string level = request["Level"].asString(); @@ -1255,6 +1277,34 @@ } + static void GetInstanceHeader(RestApiGetCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + std::string publicId = call.GetUriComponent("id", ""); + bool simplify = call.HasArgument("simplify"); + + std::string dicomContent; + context.ReadFile(dicomContent, publicId, FileContentType_Dicom); + + ParsedDicomFile dicom(dicomContent); + + Json::Value header; + dicom.HeaderToJson(header, DicomToJsonFormat_Full); + + if (simplify) + { + Json::Value simplified; + Toolbox::SimplifyTags(simplified, header); + call.GetOutput().AnswerJson(simplified); + } + else + { + call.GetOutput().AnswerJson(header); + } + } + + void OrthancRestApi::RegisterResources() { Register("/instances", ListResources<ResourceType_Instance>); @@ -1303,6 +1353,7 @@ Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>); Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>); Register("/instances/{id}/matlab", GetMatlabImage); + Register("/instances/{id}/header", GetInstanceHeader); Register("/patients/{id}/protected", IsProtectedPatient); Register("/patients/{id}/protected", SetPatientProtection);
--- a/OrthancServer/ParsedDicomFile.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -80,12 +80,12 @@ #include "ParsedDicomFile.h" +#include "OrthancInitialization.h" #include "ServerToolbox.h" #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" #include "Internals/DicomImageDecoder.h" #include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" -#include "../Core/Images/ImageBuffer.h" #include "../Core/Images/JpegWriter.h" #include "../Core/Images/JpegReader.h" #include "../Core/Images/PngReader.h" @@ -153,7 +153,8 @@ // This method can only be called from the constructors! - void ParsedDicomFile::Setup(const char* buffer, size_t size) + void ParsedDicomFile::Setup(const void* buffer, + size_t size) { DcmInputBufferStream is; if (size > 0) @@ -592,9 +593,9 @@ void ParsedDicomFile::Insert(const DicomTag& tag, const Json::Value& value, - bool decodeBinaryTags) + bool decodeDataUriScheme) { - std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeBinaryTags, GetEncoding())); + std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding())); InsertInternal(*pimpl_->file_->getDataset(), element.release()); } @@ -629,7 +630,7 @@ void ParsedDicomFile::UpdateStorageUid(const DicomTag& tag, const std::string& utf8Value, - bool decodeBinaryTags) + bool decodeDataUriScheme) { if (tag != DICOM_TAG_SOP_CLASS_UID && tag != DICOM_TAG_SOP_INSTANCE_UID) @@ -640,7 +641,7 @@ std::string binary; const std::string* decoded = &utf8Value; - if (decodeBinaryTags && + if (decodeDataUriScheme && boost::starts_with(utf8Value, "data:application/octet-stream;base64,")) { std::string mime; @@ -691,10 +692,10 @@ void ParsedDicomFile::Replace(const DicomTag& tag, const Json::Value& value, - bool decodeBinaryTags, + bool decodeDataUriScheme, DicomReplaceMode mode) { - std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeBinaryTags, GetEncoding())); + std::auto_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, GetEncoding())); ReplaceInternal(*pimpl_->file_->getDataset(), element, mode); if (tag == DICOM_TAG_SOP_CLASS_UID || @@ -705,7 +706,7 @@ throw OrthancException(ErrorCode_BadParameterType); } - UpdateStorageUid(tag, value.asString(), decodeBinaryTags); + UpdateStorageUid(tag, value.asString(), decodeDataUriScheme); } } @@ -727,7 +728,7 @@ DcmTagKey k(tag.GetGroup(), tag.GetElement()); DcmDataset& dataset = *pimpl_->file_->getDataset(); - if (FromDcmtkBridge::IsPrivateTag(tag) || + if (tag.IsPrivate() || FromDcmtkBridge::IsUnknownTag(tag) || tag == DICOM_TAG_PIXEL_DATA || tag == DICOM_TAG_ENCAPSULATED_DOCUMENT) @@ -813,17 +814,34 @@ } - ParsedDicomFile::ParsedDicomFile() : pimpl_(new PImpl) + ParsedDicomFile::ParsedDicomFile(bool createIdentifiers) : pimpl_(new PImpl) { pimpl_->file_.reset(new DcmFileFormat); - Replace(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient)); - Replace(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study)); - Replace(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series)); - Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance)); + + if (createIdentifiers) + { + Replace(DICOM_TAG_PATIENT_ID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient)); + Replace(DICOM_TAG_STUDY_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study)); + Replace(DICOM_TAG_SERIES_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series)); + Replace(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance)); + } } - ParsedDicomFile::ParsedDicomFile(const char* content, size_t size) : pimpl_(new PImpl) + ParsedDicomFile::ParsedDicomFile(const DicomMap& map) : pimpl_(new PImpl) + { + std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(map)); + + // NOTE: This implies an unnecessary memory copy of the dataset, but no way to get around + // http://support.dcmtk.org/redmine/issues/544 + std::auto_ptr<DcmFileFormat> fileFormat(new DcmFileFormat(dataset.get())); + + pimpl_->file_.reset(fileFormat.release()); + } + + + ParsedDicomFile::ParsedDicomFile(const void* content, + size_t size) : pimpl_(new PImpl) { Setup(content, size); } @@ -851,15 +869,27 @@ } + ParsedDicomFile::ParsedDicomFile(DcmDataset& dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(new DcmFileFormat(&dicom)); + } + + + ParsedDicomFile::ParsedDicomFile(DcmFileFormat& dicom) : pimpl_(new PImpl) + { + pimpl_->file_.reset(new DcmFileFormat(dicom)); + } + + ParsedDicomFile::~ParsedDicomFile() { delete pimpl_; } - void* ParsedDicomFile::GetDcmtkObject() + DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const { - return pimpl_->file_.get(); + return *pimpl_->file_.get(); } @@ -1017,69 +1047,80 @@ } - void ParsedDicomFile::ExtractImage(ImageBuffer& result, - unsigned int frame) + ImageAccessor* ParsedDicomFile::ExtractImage(IDicomImageDecoder& decoder, + unsigned int frame) { - DcmDataset& dataset = *pimpl_->file_->getDataset(); + std::auto_ptr<ImageAccessor> decoded(decoder.Decode(*this, frame)); - if (!DicomImageDecoder::Decode(result, dataset, frame)) + if (decoded.get() == NULL) { + LOG(ERROR) << "Cannot decode a DICOM image"; throw OrthancException(ErrorCode_BadFileFormat); } + else + { + return decoded.release(); + } } - void ParsedDicomFile::ExtractImage(ImageBuffer& result, - unsigned int frame, - ImageExtractionMode mode) + ImageAccessor* ParsedDicomFile::ExtractImage(IDicomImageDecoder& decoder, + unsigned int frame, + ImageExtractionMode mode) { - DcmDataset& dataset = *pimpl_->file_->getDataset(); + std::auto_ptr<ImageAccessor> decoded(ExtractImage(decoder, frame)); bool ok = false; switch (mode) { case ImageExtractionMode_UInt8: - ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale8, false); + ok = DicomImageDecoder::TruncateDecodedImage(decoded, PixelFormat_Grayscale8, false); break; case ImageExtractionMode_UInt16: - ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_Grayscale16, false); + ok = DicomImageDecoder::TruncateDecodedImage(decoded, PixelFormat_Grayscale16, false); break; case ImageExtractionMode_Int16: - ok = DicomImageDecoder::DecodeAndTruncate(result, dataset, frame, PixelFormat_SignedGrayscale16, false); + ok = DicomImageDecoder::TruncateDecodedImage(decoded, PixelFormat_SignedGrayscale16, false); break; case ImageExtractionMode_Preview: - ok = DicomImageDecoder::DecodePreview(result, dataset, frame); + ok = DicomImageDecoder::PreviewDecodedImage(decoded); break; default: throw OrthancException(ErrorCode_ParameterOutOfRange); } - if (!ok) + if (ok) { - throw OrthancException(ErrorCode_BadFileFormat); + assert(decoded.get() != NULL); + return decoded.release(); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); } } void ParsedDicomFile::ExtractPngImage(std::string& result, + IDicomImageDecoder& decoder, unsigned int frame, ImageExtractionMode mode) { - ImageBuffer buffer; - ExtractImage(buffer, frame, mode); + std::auto_ptr<ImageAccessor> decoded(ExtractImage(decoder, frame, mode)); + assert(decoded.get() != NULL); - ImageAccessor accessor(buffer.GetConstAccessor()); PngWriter writer; - writer.WriteToMemory(result, accessor); + writer.WriteToMemory(result, *decoded); } void ParsedDicomFile::ExtractJpegImage(std::string& result, + IDicomImageDecoder& decoder, unsigned int frame, ImageExtractionMode mode, uint8_t quality) @@ -1090,13 +1131,12 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - ImageBuffer buffer; - ExtractImage(buffer, frame, mode); + std::auto_ptr<ImageAccessor> decoded(ExtractImage(decoder, frame, mode)); + assert(decoded.get() != NULL); - ImageAccessor accessor(buffer.GetConstAccessor()); JpegWriter writer; writer.SetQuality(quality); - writer.WriteToMemory(result, accessor); + writer.WriteToMemory(result, *decoded); } @@ -1128,6 +1168,13 @@ } + void ParsedDicomFile::HeaderToJson(Json::Value& target, + DicomToJsonFormat format) + { + FromDcmtkBridge::ToJson(target, *pimpl_->file_->getMetaInfo(), format, DicomToJsonFlags_None, 0); + } + + bool ParsedDicomFile::HasTag(const DicomTag& tag) const { DcmTag key(tag.GetGroup(), tag.GetElement()); @@ -1222,4 +1269,60 @@ { FromDcmtkBridge::Convert(tags, *pimpl_->file_->getDataset()); } + + + ParsedDicomFile* ParsedDicomFile::CreateFromJson(const Json::Value& json, + DicomFromJsonFlags flags) + { + std::string tmp = Configuration::GetGlobalStringParameter("DefaultEncoding", "Latin1"); + Encoding encoding = StringToEncoding(tmp.c_str()); + + Json::Value::Members tags = json.getMemberNames(); + + for (size_t i = 0; i < tags.size(); i++) + { + DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]); + if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + const Json::Value& value = json[tags[i]]; + if (value.type() != Json::stringValue || + !GetDicomEncoding(encoding, value.asCString())) + { + LOG(ERROR) << "Unknown encoding while creating DICOM from JSON: " << value; + throw OrthancException(ErrorCode_BadRequest); + } + } + } + + const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers); + const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme); + + std::auto_ptr<ParsedDicomFile> result(new ParsedDicomFile(generateIdentifiers)); + result->SetEncoding(encoding); + + for (size_t i = 0; i < tags.size(); i++) + { + DicomTag tag = FromDcmtkBridge::ParseTag(tags[i]); + const Json::Value& value = json[tags[i]]; + + if (tag == DICOM_TAG_PIXEL_DATA || + tag == DICOM_TAG_ENCAPSULATED_DOCUMENT) + { + if (value.type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest); + } + else + { + result->EmbedContent(value.asString()); + } + } + else if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + result->Replace(tag, value, decodeDataUriScheme); + } + } + + return result.release(); + } }
--- a/OrthancServer/ParsedDicomFile.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ParsedDicomFile.h Wed Dec 02 09:52:56 2015 +0100 @@ -33,11 +33,13 @@ #pragma once #include "../Core/DicomFormat/DicomInstanceHasher.h" +#include "../Core/IDynamicObject.h" #include "../Core/RestApi/RestApiOutput.h" +#include "IDicomImageDecoder.h" #include "ServerEnumerations.h" -#include "../Core/Images/ImageAccessor.h" -#include "../Core/Images/ImageBuffer.h" -#include "../Core/IDynamicObject.h" + +class DcmDataset; +class DcmFileFormat; namespace Orthanc { @@ -49,26 +51,32 @@ ParsedDicomFile(ParsedDicomFile& other); - void Setup(const char* content, + void Setup(const void* content, size_t size); void RemovePrivateTagsInternal(const std::set<DicomTag>* toKeep); void UpdateStorageUid(const DicomTag& tag, const std::string& value, - bool decodeBinaryTags); + bool decodeDataUriScheme); public: - ParsedDicomFile(); // Create a minimal DICOM instance + ParsedDicomFile(bool createIdentifiers); // Create a minimal DICOM instance - ParsedDicomFile(const char* content, + ParsedDicomFile(const DicomMap& map); + + ParsedDicomFile(const void* content, size_t size); ParsedDicomFile(const std::string& content); + ParsedDicomFile(DcmDataset& dicom); + + ParsedDicomFile(DcmFileFormat& dicom); + ~ParsedDicomFile(); - void* GetDcmtkObject(); + DcmFileFormat& GetDcmtkObject() const; ParsedDicomFile* Clone(); @@ -85,12 +93,12 @@ void Replace(const DicomTag& tag, const Json::Value& value, // Assumed to be encoded with UTF-8 - bool decodeBinaryTags, + bool decodeDataUriScheme, DicomReplaceMode mode = DicomReplaceMode_InsertIfAbsent); void Insert(const DicomTag& tag, const Json::Value& value, // Assumed to be encoded with UTF-8 - bool decodeBinaryTags); + bool decodeDataUriScheme); void RemovePrivateTags() { @@ -118,18 +126,20 @@ void EmbedImage(const std::string& mime, const std::string& content); - void ExtractImage(ImageBuffer& result, - unsigned int frame); + ImageAccessor* ExtractImage(IDicomImageDecoder& decoder, + unsigned int frame); - void ExtractImage(ImageBuffer& result, - unsigned int frame, - ImageExtractionMode mode); + ImageAccessor* ExtractImage(IDicomImageDecoder& decoder, + unsigned int frame, + ImageExtractionMode mode); void ExtractPngImage(std::string& result, + IDicomImageDecoder& decoder, unsigned int frame, ImageExtractionMode mode); void ExtractJpegImage(std::string& result, + IDicomImageDecoder& decoder, unsigned int frame, ImageExtractionMode mode, uint8_t quality); @@ -143,6 +153,9 @@ DicomToJsonFlags flags, unsigned int maxStringLength); + void HeaderToJson(Json::Value& target, + DicomToJsonFormat format); + bool HasTag(const DicomTag& tag) const; void EmbedPdf(const std::string& pdf); @@ -150,6 +163,9 @@ bool ExtractPdf(std::string& pdf); void Convert(DicomMap& tags); + + static ParsedDicomFile* CreateFromJson(const Json::Value& value, + DicomFromJsonFlags flags); }; }
--- a/OrthancServer/QueryRetrieveHandler.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/QueryRetrieveHandler.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -95,31 +95,24 @@ } - const DicomMap& QueryRetrieveHandler::GetAnswer(size_t i) + void QueryRetrieveHandler::GetAnswer(DicomMap& target, + size_t i) { Run(); - - if (i >= answers_.GetSize()) - { - throw OrthancException(ErrorCode_ParameterOutOfRange); - } - - return answers_.GetAnswer(i); + answers_.GetAnswer(i).Convert(target); } void QueryRetrieveHandler::Retrieve(const std::string& target, size_t i) { - Run(); + DicomMap map; + GetAnswer(map, i); - if (i >= answers_.GetSize()) { - throw OrthancException(ErrorCode_ParameterOutOfRange); + ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); + locker.GetConnection().Move(target, map); } - - ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_); - locker.GetConnection().Move(target, answers_.GetAnswer(i)); }
--- a/OrthancServer/QueryRetrieveHandler.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/QueryRetrieveHandler.h Wed Dec 02 09:52:56 2015 +0100 @@ -85,7 +85,8 @@ size_t GetAnswerCount(); - const DicomMap& GetAnswer(size_t i); + void GetAnswer(DicomMap& target, + size_t i); void Retrieve(const std::string& target, size_t i);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/HierarchicalMatcher.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,329 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "HierarchicalMatcher.h" + +#include "../../Core/OrthancException.h" +#include "../FromDcmtkBridge.h" +#include "../ToDcmtkBridge.h" + +#include <dcmtk/dcmdata/dcfilefo.h> + +namespace Orthanc +{ + HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query, + bool caseSensitivePN) + { + Setup(*query.GetDcmtkObject().getDataset(), + caseSensitivePN, + query.GetEncoding()); + } + + + HierarchicalMatcher::~HierarchicalMatcher() + { + for (Constraints::iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + if (it->second != NULL) + { + delete it->second; + } + } + + for (Sequences::iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + if (it->second != NULL) + { + delete it->second; + } + } + } + + + void HierarchicalMatcher::Setup(DcmItem& dataset, + bool caseSensitivePN, + Encoding encoding) + { + for (unsigned long i = 0; i < dataset.card(); i++) + { + DcmElement* element = dataset.getElement(i); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DicomTag tag(FromDcmtkBridge::Convert(element->getTag())); + if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET) + { + // Ignore this specific tag + continue; + } + + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + if (constraints_.find(tag) != constraints_.end() || + sequences_.find(tag) != sequences_.end()) + { + throw OrthancException(ErrorCode_BadRequest); + } + + if (vr == ValueRepresentation_Sequence) + { + DcmSequenceOfItems& sequence = dynamic_cast<DcmSequenceOfItems&>(*element); + + if (sequence.card() == 0 || + (sequence.card() == 1 && sequence.getItem(0)->card() == 0)) + { + // Universal matching of a sequence + sequences_[tag] = NULL; + } + else if (sequence.card() == 1) + { + sequences_[tag] = new HierarchicalMatcher(*sequence.getItem(0), caseSensitivePN, encoding); + } + else + { + throw OrthancException(ErrorCode_BadRequest); + } + } + else + { + std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement + (*element, DicomToJsonFlags_None, encoding)); + + if (value->IsBinary() || + value->IsNull()) + { + throw OrthancException(ErrorCode_BadRequest); + } + else if (value->GetContent().empty()) + { + // This is an universal matcher + constraints_[tag] = NULL; + } + else + { + // DICOM specifies that searches must be case sensitive, except + // for tags with a PN value representation + bool sensitive = true; + if (vr == ValueRepresentation_PatientName) + { + sensitive = caseSensitivePN; + } + + constraints_[tag] = IFindConstraint::ParseDicomConstraint(tag, value->GetContent(), sensitive); + } + } + } + } + + + std::string HierarchicalMatcher::Format(const std::string& prefix) const + { + std::string s; + + for (Constraints::const_iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + s += prefix + it->first.Format() + " "; + + if (it->second == NULL) + { + s += "*\n"; + } + else + { + s += it->second->Format() + "\n"; + } + } + + for (Sequences::const_iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + s += prefix + it->first.Format() + " "; + + if (it->second == NULL) + { + s += "*\n"; + } + else + { + s += "Sequence:\n" + it->second->Format(prefix + " "); + } + } + + return s; + } + + + bool HierarchicalMatcher::Match(ParsedDicomFile& dicom) const + { + return MatchInternal(*dicom.GetDcmtkObject().getDataset(), + dicom.GetEncoding()); + } + + + bool HierarchicalMatcher::MatchInternal(DcmItem& item, + Encoding encoding) const + { + for (Constraints::const_iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + if (it->second != NULL) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmElement* element = NULL; + if (!item.findAndGetElement(tag, element).good() || + element == NULL) + { + return false; + } + + std::auto_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement + (*element, DicomToJsonFlags_None, encoding)); + + if (value->IsNull() || + value->IsBinary() || + !it->second->Match(value->GetContent())) + { + return false; + } + } + } + + for (Sequences::const_iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + if (it->second != NULL) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmSequenceOfItems* sequence = NULL; + if (!item.findAndGetSequence(tag, sequence).good() || + sequence == NULL) + { + return true; + } + + bool match = false; + + for (unsigned long i = 0; i < sequence->card(); i++) + { + if (it->second->MatchInternal(*sequence->getItem(i), encoding)) + { + match = true; + break; + } + } + + if (!match) + { + return false; + } + } + } + + return true; + } + + + DcmDataset* HierarchicalMatcher::ExtractInternal(DcmItem& source, + Encoding encoding) const + { + std::auto_ptr<DcmDataset> target(new DcmDataset); + + for (Constraints::const_iterator it = constraints_.begin(); + it != constraints_.end(); ++it) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmElement* element = NULL; + if (source.findAndGetElement(tag, element).good() && + element != NULL) + { + std::auto_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(it->first)); + cloned->copyFrom(*element); + target->insert(cloned.release()); + } + } + + for (Sequences::const_iterator it = sequences_.begin(); + it != sequences_.end(); ++it) + { + DcmTagKey tag = ToDcmtkBridge::Convert(it->first); + + DcmSequenceOfItems* sequence = NULL; + if (source.findAndGetSequence(tag, sequence).good() && + sequence != NULL) + { + std::auto_ptr<DcmSequenceOfItems> cloned(new DcmSequenceOfItems(tag)); + + for (unsigned long i = 0; i < sequence->card(); i++) + { + if (it->second == NULL) + { + cloned->append(new DcmItem(*sequence->getItem(i))); + } + else if (it->second->MatchInternal(*sequence->getItem(i), encoding)) // TODO Might be optimized + { + // It is necessary to encapsulate the child dataset into a + // "DcmItem" object before it can be included in a + // sequence. Otherwise, "dciodvfy" reports an error "Bad + // tag in sequence - Expecting Item or Sequence Delimiter." + std::auto_ptr<DcmDataset> child(it->second->ExtractInternal(*sequence->getItem(i), encoding)); + cloned->append(new DcmItem(*child)); + } + } + + target->insert(cloned.release()); + } + } + + return target.release(); + } + + + ParsedDicomFile* HierarchicalMatcher::Extract(ParsedDicomFile& dicom) const + { + std::auto_ptr<DcmDataset> dataset(ExtractInternal(*dicom.GetDcmtkObject().getDataset(), + dicom.GetEncoding())); + + std::auto_ptr<ParsedDicomFile> result(new ParsedDicomFile(*dataset)); + result->SetEncoding(Encoding_Utf8); + + return result.release(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/HierarchicalMatcher.h Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,81 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../Core/DicomFormat/DicomMap.h" +#include "IFindConstraint.h" +#include "../ParsedDicomFile.h" + +class DcmItem; + +namespace Orthanc +{ + class HierarchicalMatcher : public boost::noncopyable + { + private: + typedef std::map<DicomTag, IFindConstraint*> Constraints; + typedef std::map<DicomTag, HierarchicalMatcher*> Sequences; + + Constraints constraints_; + Sequences sequences_; + + void Setup(DcmItem& query, + bool caseSensitivePN, + Encoding encoding); + + HierarchicalMatcher(DcmItem& query, + bool caseSensitivePN, + Encoding encoding) + { + Setup(query, caseSensitivePN, encoding); + } + + bool MatchInternal(DcmItem& dicom, + Encoding encoding) const; + + DcmDataset* ExtractInternal(DcmItem& dicom, + Encoding encoding) const; + + public: + HierarchicalMatcher(ParsedDicomFile& query, + bool caseSensitivePN); + + ~HierarchicalMatcher(); + + std::string Format(const std::string& prefix = "") const; + + bool Match(ParsedDicomFile& dicom) const; + + ParsedDicomFile* Extract(ParsedDicomFile& dicom) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Search/IFindConstraint.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,130 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "IFindConstraint.h" + +#include "ListConstraint.h" +#include "RangeConstraint.h" +#include "ValueConstraint.h" +#include "WildcardConstraint.h" + +#include "../FromDcmtkBridge.h" +#include "../../Core/OrthancException.h" + +namespace Orthanc +{ + IFindConstraint* IFindConstraint::ParseDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive) + { + ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); + + if (vr == ValueRepresentation_Sequence) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if ((vr == ValueRepresentation_Date || + vr == ValueRepresentation_DateTime || + vr == ValueRepresentation_Time) && + dicomQuery.find('-') != std::string::npos) + { + /** + * Range matching is only defined for TM, DA and DT value + * representations. This code fixes issues 35 and 37. + * + * Reference: "Range matching is not defined for types of + * Attributes other than dates and times", DICOM PS 3.4, + * C.2.2.2.5 ("Range Matching"). + **/ + size_t separator = dicomQuery.find('-'); + std::string lower = dicomQuery.substr(0, separator); + std::string upper = dicomQuery.substr(separator + 1); + return new RangeConstraint(lower, upper, caseSensitive); + } + else if (dicomQuery.find('\\') != std::string::npos) + { + std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive)); + + std::vector<std::string> items; + Toolbox::TokenizeString(items, dicomQuery, '\\'); + + for (size_t i = 0; i < items.size(); i++) + { + constraint->AddAllowedValue(items[i]); + } + + return constraint.release(); + } + else if (dicomQuery.find('*') != std::string::npos || + dicomQuery.find('?') != std::string::npos) + { + return new WildcardConstraint(dicomQuery, caseSensitive); + } + else + { + /** + * Case-insensitive match for PN value representation (Patient + * Name). Case-senstive match for all the other value + * representations. + * + * Reference: DICOM PS 3.4 + * - C.2.2.2.1 ("Single Value Matching") + * - C.2.2.2.4 ("Wild Card Matching") + * http://medical.nema.org/Dicom/2011/11_04pu.pdf + * + * "Except for Attributes with a PN Value Representation, only + * entities with values which match exactly the value specified in the + * request shall match. This matching is case-sensitive, i.e., + * sensitive to the exact encoding of the key attribute value in + * character sets where a letter may have multiple encodings (e.g., + * based on its case, its position in a word, or whether it is + * accented) + * + * For Attributes with a PN Value Representation (e.g., Patient Name + * (0010,0010)), an application may perform literal matching that is + * either case-sensitive, or that is insensitive to some or all + * aspects of case, position, accent, or other character encoding + * variants." + * + * (0008,0018) UI SOPInstanceUID => Case-sensitive + * (0008,0050) SH AccessionNumber => Case-sensitive + * (0010,0020) LO PatientID => Case-sensitive + * (0020,000D) UI StudyInstanceUID => Case-sensitive + * (0020,000E) UI SeriesInstanceUID => Case-sensitive + **/ + + return new ValueConstraint(dicomQuery, caseSensitive); + } + } +}
--- a/OrthancServer/Search/IFindConstraint.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/IFindConstraint.h Wed Dec 02 09:52:56 2015 +0100 @@ -49,5 +49,11 @@ const DicomTag& tag) const = 0; virtual bool Match(const std::string& value) const = 0; + + virtual std::string Format() const = 0; + + static IFindConstraint* ParseDicomConstraint(const DicomTag& tag, + const std::string& dicomQuery, + bool caseSensitive); }; }
--- a/OrthancServer/Search/ListConstraint.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/ListConstraint.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -75,4 +75,23 @@ return allowedValues_.find(v) != allowedValues_.end(); } + + + std::string ListConstraint::Format() const + { + std::string s; + + for (std::set<std::string>::const_iterator + it = allowedValues_.begin(); it != allowedValues_.end(); ++it) + { + if (!s.empty()) + { + s += "\\"; + } + + s += *it; + } + + return s; + } }
--- a/OrthancServer/Search/ListConstraint.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/ListConstraint.h Wed Dec 02 09:52:56 2015 +0100 @@ -67,5 +67,7 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const; }; }
--- a/OrthancServer/Search/LookupResource.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/LookupResource.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -33,11 +33,6 @@ #include "../PrecompiledHeadersServer.h" #include "LookupResource.h" -#include "ListConstraint.h" -#include "RangeConstraint.h" -#include "ValueConstraint.h" -#include "WildcardConstraint.h" - #include "../../Core/OrthancException.h" #include "../../Core/FileStorage/StorageAccessor.h" #include "../ServerToolbox.h" @@ -426,85 +421,16 @@ const std::string& dicomQuery, bool caseSensitive) { - ValueRepresentation vr = FromDcmtkBridge::GetValueRepresentation(tag); - // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html if (tag == DICOM_TAG_MODALITIES_IN_STUDY) { SetModalitiesInStudy(dicomQuery); } - else if ((vr == ValueRepresentation_Date || - vr == ValueRepresentation_DateTime || - vr == ValueRepresentation_Time) && - dicomQuery.find('-') != std::string::npos) - { - /** - * Range matching is only defined for TM, DA and DT value - * representations. This code fixes issues 35 and 37. - * - * Reference: "Range matching is not defined for types of - * Attributes other than dates and times", DICOM PS 3.4, - * C.2.2.2.5 ("Range Matching"). - **/ - size_t separator = dicomQuery.find('-'); - std::string lower = dicomQuery.substr(0, separator); - std::string upper = dicomQuery.substr(separator + 1); - Add(tag, new RangeConstraint(lower, upper, caseSensitive)); - } - else if (dicomQuery.find('\\') != std::string::npos) - { - std::auto_ptr<ListConstraint> constraint(new ListConstraint(caseSensitive)); - - std::vector<std::string> items; - Toolbox::TokenizeString(items, dicomQuery, '\\'); - - for (size_t i = 0; i < items.size(); i++) - { - constraint->AddAllowedValue(items[i]); - } - - Add(tag, constraint.release()); - } - else if (dicomQuery.find('*') != std::string::npos || - dicomQuery.find('?') != std::string::npos) + else { - Add(tag, new WildcardConstraint(dicomQuery, caseSensitive)); - } - else - { - /** - * Case-insensitive match for PN value representation (Patient - * Name). Case-senstive match for all the other value - * representations. - * - * Reference: DICOM PS 3.4 - * - C.2.2.2.1 ("Single Value Matching") - * - C.2.2.2.4 ("Wild Card Matching") - * http://medical.nema.org/Dicom/2011/11_04pu.pdf - * - * "Except for Attributes with a PN Value Representation, only - * entities with values which match exactly the value specified in the - * request shall match. This matching is case-sensitive, i.e., - * sensitive to the exact encoding of the key attribute value in - * character sets where a letter may have multiple encodings (e.g., - * based on its case, its position in a word, or whether it is - * accented) - * - * For Attributes with a PN Value Representation (e.g., Patient Name - * (0010,0010)), an application may perform literal matching that is - * either case-sensitive, or that is insensitive to some or all - * aspects of case, position, accent, or other character encoding - * variants." - * - * (0008,0018) UI SOPInstanceUID => Case-sensitive - * (0008,0050) SH AccessionNumber => Case-sensitive - * (0010,0020) LO PatientID => Case-sensitive - * (0020,000D) UI StudyInstanceUID => Case-sensitive - * (0020,000E) UI SeriesInstanceUID => Case-sensitive - **/ - - Add(tag, new ValueConstraint(dicomQuery, caseSensitive)); + Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive)); } } + }
--- a/OrthancServer/Search/RangeConstraint.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/RangeConstraint.h Wed Dec 02 09:52:56 2015 +0100 @@ -64,5 +64,10 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const + { + return lower_ + "-" + upper_; + } }; }
--- a/OrthancServer/Search/ValueConstraint.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/ValueConstraint.h Wed Dec 02 09:52:56 2015 +0100 @@ -61,5 +61,10 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const + { + return value_; + } }; }
--- a/OrthancServer/Search/WildcardConstraint.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/WildcardConstraint.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -78,4 +78,9 @@ { lookup.AddConstraint(tag, IdentifierConstraintType_Wildcard, pimpl_->wildcard_); } + + std::string WildcardConstraint::Format() const + { + return pimpl_->wildcard_; + } }
--- a/OrthancServer/Search/WildcardConstraint.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/Search/WildcardConstraint.h Wed Dec 02 09:52:56 2015 +0100 @@ -59,5 +59,7 @@ const DicomTag& tag) const; virtual bool Match(const std::string& value) const; + + virtual std::string Format() const; }; }
--- a/OrthancServer/ServerContext.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ServerContext.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -236,8 +236,7 @@ typedef std::map<MetadataType, std::string> InstanceMetadata; InstanceMetadata instanceMetadata; - StoreStatus status = index_.Store(instanceMetadata, dicom.GetSummary(), attachments, - dicom.GetRemoteAet(), dicom.GetMetadata()); + StoreStatus status = index_.Store(instanceMetadata, dicom, attachments); // Only keep the metadata for the "instance" level dicom.GetMetadata().clear();
--- a/OrthancServer/ServerEnumerations.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ServerEnumerations.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -63,6 +63,7 @@ dictMetadataType_.Add(MetadataType_ModifiedFrom, "ModifiedFrom"); dictMetadataType_.Add(MetadataType_AnonymizedFrom, "AnonymizedFrom"); dictMetadataType_.Add(MetadataType_LastUpdate, "LastUpdate"); + dictMetadataType_.Add(MetadataType_Instance_Origin, "Origin"); dictContentType_.Add(FileContentType_Dicom, "dicom"); dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json"); @@ -336,6 +337,15 @@ case ModalityManufacturer_SyngoVia: return "SyngoVia"; + case ModalityManufacturer_AgfaImpax: + return "AgfaImpax"; + + case ModalityManufacturer_EFilm2: + return "EFilm2"; + + case ModalityManufacturer_Vitrea: + return "Vitrea"; + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } @@ -399,6 +409,18 @@ { return ModalityManufacturer_SyngoVia; } + else if (manufacturer == "AgfaImpax") + { + return ModalityManufacturer_AgfaImpax; + } + else if (manufacturer == "Vitrea") + { + return ModalityManufacturer_Vitrea; + } + else if (manufacturer == "EFilm2") + { + return ModalityManufacturer_EFilm2; + } else { throw OrthancException(ErrorCode_ParameterOutOfRange);
--- a/OrthancServer/ServerEnumerations.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ServerEnumerations.h Wed Dec 02 09:52:56 2015 +0100 @@ -62,7 +62,10 @@ ModalityManufacturer_ClearCanvas, ModalityManufacturer_MedInria, ModalityManufacturer_Dcm4Chee, - ModalityManufacturer_SyngoVia + ModalityManufacturer_SyngoVia, + ModalityManufacturer_AgfaImpax, + ModalityManufacturer_EFilm2, + ModalityManufacturer_Vitrea }; enum DicomRequestType @@ -98,7 +101,8 @@ ValueRepresentation_PatientName, ValueRepresentation_Date, ValueRepresentation_DateTime, - ValueRepresentation_Time + ValueRepresentation_Time, + ValueRepresentation_Sequence }; enum DicomToJsonFormat @@ -119,12 +123,19 @@ // Some predefined combinations DicomToJsonFlags_None = 0, - DicomToJsonFlags_Default = (DicomToJsonFlags_IncludePrivateTags | + DicomToJsonFlags_Default = (DicomToJsonFlags_IncludeBinary | + DicomToJsonFlags_IncludePixelData | + DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeUnknownTags | - DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull) }; + enum DicomFromJsonFlags + { + DicomFromJsonFlags_DecodeDataUriScheme = (1 << 0), + DicomFromJsonFlags_GenerateIdentifiers = (1 << 1) + }; + enum IdentifierConstraintType { IdentifierConstraintType_Equal, @@ -156,6 +167,7 @@ MetadataType_ModifiedFrom = 5, MetadataType_AnonymizedFrom = 6, MetadataType_LastUpdate = 7, + MetadataType_Instance_Origin = 8, // New in Orthanc 0.9.5 // Make sure that the value "65535" can be stored into this enumeration MetadataType_StartUser = 1024,
--- a/OrthancServer/ServerIndex.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ServerIndex.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -50,6 +50,7 @@ #include "FromDcmtkBridge.h" #include "ServerContext.h" +#include "DicomInstanceToStore.h" #include <boost/lexical_cast.hpp> #include <stdio.h> @@ -593,16 +594,17 @@ StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata, - const DicomMap& dicomSummary, - const Attachments& attachments, - const std::string& remoteAet, - const MetadataMap& metadata) + DicomInstanceToStore& instanceToStore, + const Attachments& attachments) { boost::mutex::scoped_lock lock(mutex_); + const DicomMap& dicomSummary = instanceToStore.GetSummary(); + const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata(); + instanceMetadata.clear(); - DicomInstanceHasher hasher(dicomSummary); + DicomInstanceHasher hasher(instanceToStore.GetSummary()); try { @@ -766,8 +768,14 @@ db_.SetMetadata(instance, MetadataType_Instance_ReceptionDate, now); instanceMetadata[MetadataType_Instance_ReceptionDate] = now; - db_.SetMetadata(instance, MetadataType_Instance_RemoteAet, remoteAet); - instanceMetadata[MetadataType_Instance_RemoteAet] = remoteAet; + db_.SetMetadata(instance, MetadataType_Instance_RemoteAet, instanceToStore.GetRemoteAet()); + instanceMetadata[MetadataType_Instance_RemoteAet] = instanceToStore.GetRemoteAet(); + + { + std::string s = EnumerationToString(instanceToStore.GetRequestOrigin()); + db_.SetMetadata(instance, MetadataType_Instance_Origin, s); + instanceMetadata[MetadataType_Instance_Origin] = s; + } const DicomValue* value; if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
--- a/OrthancServer/ServerIndex.h Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ServerIndex.h Wed Dec 02 09:52:56 2015 +0100 @@ -42,11 +42,11 @@ #include "IDatabaseWrapper.h" - namespace Orthanc { class LookupResource; class ServerContext; + class DicomInstanceToStore; class ServerIndex : public boost::noncopyable { @@ -140,10 +140,8 @@ void SetMaximumPatientCount(unsigned int count); StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata, - const DicomMap& dicomSummary, - const Attachments& attachments, - const std::string& remoteAet, - const MetadataMap& metadata); + DicomInstanceToStore& instance, + const Attachments& attachments); void ComputeStatistics(Json::Value& target);
--- a/OrthancServer/ServerToolbox.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/ServerToolbox.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -297,7 +297,7 @@ database.GetAllPublicIds(resources, level); for (std::list<std::string>::const_iterator - it = resources.begin(); it != resources.end(); it++) + it = resources.begin(); it != resources.end(); ++it) { // Locate the resource and one of its child instances int64_t resource, instance;
--- a/OrthancServer/SliceOrdering.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/SliceOrdering.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -276,21 +276,23 @@ PositionComparator comparator(normal_); std::sort(instances_.begin(), instances_.end(), comparator); - float a = instances_.front()->ComputeRelativePosition(normal_); - float b = instances_.back()->ComputeRelativePosition(normal_); - - if (std::fabs(b - a) <= 10.0f * std::numeric_limits<float>::epsilon()) + float a = instances_[0]->ComputeRelativePosition(normal_); + for (size_t i = 1; i < instances_.size(); i++) { - // Not enough difference between the minimum and maximum - // positions along the normal of the volume - return false; + float b = instances_[i]->ComputeRelativePosition(normal_); + + if (std::fabs(b - a) <= 10.0f * std::numeric_limits<float>::epsilon()) + { + // Not enough space between two slices along the normal of the volume + return false; + } + + a = b; } - else - { - // This is a 3D volume - isVolume_ = true; - return true; - } + + // This is a 3D volume + isVolume_ = true; + return true; }
--- a/OrthancServer/main.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/OrthancServer/main.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -159,14 +159,16 @@ { } - virtual bool IsAllowedConnection(const std::string& /*callingIp*/, - const std::string& /*callingAet*/) + virtual bool IsAllowedConnection(const std::string& /*remoteIp*/, + const std::string& /*remoteAet*/, + const std::string& /*calledAet*/) { return true; } - virtual bool IsAllowedRequest(const std::string& /*callingIp*/, - const std::string& callingAet, + virtual bool IsAllowedRequest(const std::string& /*remoteIp*/, + const std::string& remoteAet, + const std::string& /*calledAet*/, DicomRequestType type) { if (type == DicomRequestType_Store) @@ -175,9 +177,9 @@ return true; } - if (!Configuration::IsKnownAETitle(callingAet)) + if (!Configuration::IsKnownAETitle(remoteAet)) { - LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << callingAet << "\""; + LOG(ERROR) << "Unknown remote DICOM modality AET: \"" << remoteAet << "\""; return false; } else @@ -186,8 +188,9 @@ } } - virtual bool IsAllowedTransferSyntax(const std::string& callingIp, - const std::string& callingAet, + virtual bool IsAllowedTransferSyntax(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet, TransferSyntax syntax) { std::string configuration; @@ -234,8 +237,34 @@ if (locker.GetLua().IsExistingFunction(lua.c_str())) { LuaFunctionCall call(locker.GetLua(), lua.c_str()); - call.PushString(callingAet); - call.PushString(callingIp); + call.PushString(remoteAet); + call.PushString(remoteIp); + call.PushString(calledAet); + return call.ExecutePredicate(); + } + } + + return Configuration::GetGlobalBoolParameter(configuration, true); + } + + + virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + static const char* configuration = "UnknownSopClassAccepted"; + + { + std::string lua = "Is" + std::string(configuration); + + LuaScripting::Locker locker(context_.GetLua()); + + if (locker.GetLua().IsExistingFunction(lua.c_str())) + { + LuaFunctionCall call(locker.GetLua(), lua.c_str()); + call.PushString(remoteAet); + call.PushString(remoteIp); + call.PushString(calledAet); return call.ExecutePredicate(); } } @@ -550,6 +579,7 @@ PrintErrorCode(ErrorCode_DatabaseNotInitialized, "Plugin trying to call the database during its initialization"); PrintErrorCode(ErrorCode_SslDisabled, "Orthanc has been built without SSL support"); PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series"); + PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP"); } std::cout << std::endl; @@ -704,12 +734,22 @@ dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); dicomServer.SetFindRequestHandlerFactory(serverFactory); + +#if ORTHANC_PLUGINS_ENABLED == 1 + if (plugins && + plugins->HasWorklistHandler()) + { + dicomServer.SetWorklistRequestHandlerFactory(*plugins); + } +#endif + dicomServer.SetPortNumber(Configuration::GetGlobalIntegerParameter("DicomPort", 4242)); dicomServer.SetApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC")); dicomServer.SetApplicationEntityFilter(dicomFilter); dicomServer.Start(); - LOG(WARNING) << "DICOM server listening on port: " << dicomServer.GetPortNumber(); + LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle() + << " on port: " << dicomServer.GetPortNumber(); bool restart; ErrorCode error = ErrorCode_Success; @@ -1068,6 +1108,23 @@ { OrthancInitialize(configurationFile); + if (0) + { + // TODO REMOVE THIS TEST + DicomUserConnection c; + c.SetRemoteHost("localhost"); + c.SetRemotePort(4243); + c.SetRemoteApplicationEntityTitle("ORTHANCTEST"); + c.Open(); + ParsedDicomFile f(false); + f.Replace(DICOM_TAG_PATIENT_NAME, "M*"); + DicomFindAnswers a; + c.FindWorklist(a, f); + Json::Value j; + a.ToJson(j, true); + std::cout << j; + } + bool restart = StartOrthanc(argc, argv, allowDatabaseUpgrade); if (restart) {
--- a/Plugins/Engine/OrthancPlugins.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Engine/OrthancPlugins.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -47,6 +47,8 @@ #include "../../OrthancServer/OrthancInitialization.h" #include "../../OrthancServer/ServerContext.h" #include "../../OrthancServer/ServerToolbox.h" +#include "../../OrthancServer/Search/HierarchicalMatcher.h" +#include "../../OrthancServer/Internals/DicomImageDecoder.h" #include "../../Core/Compression/ZlibCompressor.h" #include "../../Core/Compression/GzipCompressor.h" #include "../../Core/Images/Image.h" @@ -61,6 +63,46 @@ namespace Orthanc { + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + const void* data, + size_t size) + { + target.size = size; + + if (size == 0) + { + target.data = NULL; + } + else + { + target.data = malloc(size); + if (target.data != NULL) + { + memcpy(target.data, data, size); + } + else + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + } + } + + + static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, + const std::string& str) + { + if (str.size() == 0) + { + target.size = 0; + target.data = NULL; + } + else + { + CopyToMemoryBuffer(target, str.c_str(), str.size()); + } + } + + namespace { class PluginStorageArea : public IStorageArea @@ -244,10 +286,14 @@ RestCallbacks restCallbacks_; OnStoredCallbacks onStoredCallbacks_; OnChangeCallbacks onChangeCallbacks_; + OrthancPluginWorklistCallback worklistCallback_; + OrthancPluginDecodeImageCallback decodeImageCallback_; std::auto_ptr<StorageAreaFactory> storageArea_; boost::recursive_mutex restCallbackMutex_; boost::recursive_mutex storedCallbackMutex_; boost::recursive_mutex changeCallbackMutex_; + boost::mutex worklistCallbackMutex_; + boost::mutex decodeImageCallbackMutex_; boost::recursive_mutex invokeServiceMutex_; Properties properties_; int argc_; @@ -257,6 +303,8 @@ PImpl() : context_(NULL), + worklistCallback_(NULL), + decodeImageCallback_(NULL), argc_(1), argv_(NULL) { @@ -265,6 +313,86 @@ + class OrthancPlugins::WorklistHandler : public IWorklistRequestHandler + { + private: + OrthancPlugins& that_; + std::auto_ptr<HierarchicalMatcher> matcher_; + ParsedDicomFile* currentQuery_; + + void Reset() + { + matcher_.reset(NULL); + currentQuery_ = NULL; + } + + public: + WorklistHandler(OrthancPlugins& that) : that_(that) + { + Reset(); + } + + virtual void Handle(DicomFindAnswers& answers, + ParsedDicomFile& query, + const std::string& remoteIp, + const std::string& remoteAet, + const std::string& calledAet) + { + bool caseSensitivePN = Configuration::GetGlobalBoolParameter("CaseSensitivePN", false); + matcher_.reset(new HierarchicalMatcher(query, caseSensitivePN)); + currentQuery_ = &query; + + { + boost::mutex::scoped_lock lock(that_.pimpl_->worklistCallbackMutex_); + + if (that_.pimpl_->worklistCallback_) + { + OrthancPluginErrorCode error = that_.pimpl_->worklistCallback_ + (reinterpret_cast<OrthancPluginWorklistAnswers*>(&answers), + reinterpret_cast<const OrthancPluginWorklistQuery*>(this), + remoteAet.c_str(), + calledAet.c_str()); + + if (error != OrthancPluginErrorCode_Success) + { + Reset(); + that_.GetErrorDictionary().LogError(error, true); + throw OrthancException(static_cast<ErrorCode>(error)); + } + } + } + + Reset(); + } + + void GetDicomQuery(OrthancPluginMemoryBuffer& target) const + { + assert(currentQuery_ != NULL); + std::string dicom; + currentQuery_->SaveToMemoryBuffer(dicom); + CopyToMemoryBuffer(target, dicom.c_str(), dicom.size()); + } + + bool IsMatch(const void* dicom, + size_t size) const + { + assert(matcher_.get() != NULL); + ParsedDicomFile f(dicom, size); + return matcher_->Match(f); + } + + void AddAnswer(OrthancPluginWorklistAnswers* answers, + const void* dicom, + size_t size) const + { + assert(matcher_.get() != NULL); + ParsedDicomFile f(dicom, size); + std::auto_ptr<ParsedDicomFile> summary(matcher_->Extract(f)); + reinterpret_cast<DicomFindAnswers*>(answers)->Add(*summary); + } + }; + + static char* CopyString(const std::string& str) { char *result = reinterpret_cast<char*>(malloc(str.size() + 1)); @@ -288,6 +416,7 @@ OrthancPlugins::OrthancPlugins() { + /* Sanity check of the compiler */ if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) || sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) || sizeof(int32_t) != sizeof(_OrthancPluginService) || @@ -301,16 +430,20 @@ sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) || + sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) || sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType) || sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || + sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) || static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePixelData) != static_cast<int>(DicomToJsonFlags_IncludePixelData) || static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToNull) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToNull) || - static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToAscii)) + static_cast<int>(OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii) != static_cast<int>(DicomToJsonFlags_ConvertBinaryToAscii) || + static_cast<int>(OrthancPluginCreateDicomFlags_DecodeDataUriScheme) != static_cast<int>(DicomFromJsonFlags_DecodeDataUriScheme) || + static_cast<int>(OrthancPluginCreateDicomFlags_GenerateIdentifiers) != static_cast<int>(DicomFromJsonFlags_GenerateIdentifiers)) + { - /* Sanity check of the compiler */ throw OrthancException(ErrorCode_Plugin); } @@ -546,46 +679,6 @@ - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, - const void* data, - size_t size) - { - target.size = size; - - if (size == 0) - { - target.data = NULL; - } - else - { - target.data = malloc(size); - if (target.data != NULL) - { - memcpy(target.data, data, size); - } - else - { - throw OrthancException(ErrorCode_NotEnoughMemory); - } - } - } - - - static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target, - const std::string& str) - { - if (str.size() == 0) - { - target.size = 0; - target.data = NULL; - } - else - { - CopyToMemoryBuffer(target, str.c_str(), str.size()); - } - } - - void OrthancPlugins::RegisterRestCallback(const void* parameters, bool lock) { @@ -622,6 +715,47 @@ } + void OrthancPlugins::RegisterWorklistCallback(const void* parameters) + { + const _OrthancPluginWorklistCallback& p = + *reinterpret_cast<const _OrthancPluginWorklistCallback*>(parameters); + + boost::mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_); + + if (pimpl_->worklistCallback_ != NULL) + { + LOG(ERROR) << "Can only register one plugin to handle modality worklists"; + throw OrthancException(ErrorCode_Plugin); + } + else + { + LOG(INFO) << "Plugin has registered a callback to handle modality worklists"; + pimpl_->worklistCallback_ = p.callback; + } + } + + + void OrthancPlugins::RegisterDecodeImageCallback(const void* parameters) + { + const _OrthancPluginDecodeImageCallback& p = + *reinterpret_cast<const _OrthancPluginDecodeImageCallback*>(parameters); + + boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + + if (pimpl_->decodeImageCallback_ != NULL) + { + LOG(ERROR) << "Can only register one plugin to handle the decompression of DICOM images"; + throw OrthancException(ErrorCode_Plugin); + } + else + { + LOG(INFO) << "Plugin has registered a callback to decode DICOM images"; + pimpl_->decodeImageCallback_ = p.callback; + } + } + + + void OrthancPlugins::AnswerBuffer(const void* parameters) { @@ -1061,6 +1195,10 @@ return; } + case _OrthancPluginService_GetInstanceOrigin: // New in Orthanc 0.9.5 + *p.resultOrigin = Plugins::Convert(instance.GetRequestOrigin()); + return; + default: throw OrthancException(ErrorCode_InternalError); } @@ -1125,6 +1263,25 @@ } + static OrthancPluginImage* ReturnImage(std::auto_ptr<ImageAccessor>& image) + { + // 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->IsReadOnly()) + { + std::auto_ptr<Image> copy(new Image(image->GetFormat(), image->GetWidth(), image->GetHeight())); + ImageProcessing::Copy(*copy, *image); + image.reset(NULL); + return reinterpret_cast<OrthancPluginImage*>(copy.release()); + } + else + { + return reinterpret_cast<OrthancPluginImage*>(image.release()); + } + } + + void OrthancPlugins::UncompressImage(const void* parameters) { const _OrthancPluginUncompressImage& p = *reinterpret_cast<const _OrthancPluginUncompressImage*>(parameters); @@ -1147,11 +1304,18 @@ break; } + case OrthancPluginImageFormat_Dicom: + { + ParsedDicomFile dicom(p.data, p.size); + image.reset(Decode(dicom, 0)); + break; + } + default: throw OrthancException(ErrorCode_ParameterOutOfRange); } - *(p.target) = reinterpret_cast<OrthancPluginImage*>(image.release()); + *(p.target) = ReturnImage(image); } @@ -1241,7 +1405,7 @@ std::auto_ptr<ImageAccessor> target(new Image(Plugins::Convert(p.targetFormat), source.GetWidth(), source.GetHeight())); ImageProcessing::Convert(*target, source); - *(p.target) = reinterpret_cast<OrthancPluginImage*>(target.release()); + *(p.target) = ReturnImage(target); } @@ -1311,6 +1475,104 @@ } + void OrthancPlugins::ApplyCreateDicom(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginCreateDicom& p = + *reinterpret_cast<const _OrthancPluginCreateDicom*>(parameters); + + Json::Value json; + + if (p.json == NULL) + { + json = Json::objectValue; + } + else + { + Json::Reader reader; + if (!reader.parse(p.json, json)) + { + throw OrthancException(ErrorCode_BadJson); + } + } + + std::string dicom; + + { + std::auto_ptr<ParsedDicomFile> file + (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(p.flags))); + + if (p.pixelData) + { + file->EmbedImage(*reinterpret_cast<const ImageAccessor*>(p.pixelData)); + } + + file->SaveToMemoryBuffer(dicom); + } + + CopyToMemoryBuffer(*p.target, dicom); + } + + + void OrthancPlugins::ComputeHash(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginComputeHash& p = + *reinterpret_cast<const _OrthancPluginComputeHash*>(parameters); + + std::string hash; + switch (service) + { + case _OrthancPluginService_ComputeMd5: + Toolbox::ComputeMD5(hash, p.buffer, p.size); + break; + + case _OrthancPluginService_ComputeSha1: + Toolbox::ComputeSHA1(hash, p.buffer, p.size); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + *p.result = CopyString(hash); + } + + + void OrthancPlugins::ApplyCreateImage(_OrthancPluginService service, + const void* parameters) + { + const _OrthancPluginCreateImage& p = + *reinterpret_cast<const _OrthancPluginCreateImage*>(parameters); + + std::auto_ptr<ImageAccessor> result; + + switch (service) + { + case _OrthancPluginService_CreateImage: + result.reset(new Image(Plugins::Convert(p.format), p.width, p.height)); + break; + + case _OrthancPluginService_CreateImageAccessor: + result.reset(new ImageAccessor); + result->AssignWritable(Plugins::Convert(p.format), p.width, p.height, p.pitch, p.buffer); + break; + + case _OrthancPluginService_DecodeDicomImage: + { + ParsedDicomFile dicom(p.constBuffer, p.bufferSize); + result.reset(Decode(dicom, p.frameIndex)); + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + *(p.target) = ReturnImage(result); + } + + void OrthancPlugins::DatabaseAnswer(const void* parameters) { const _OrthancPluginDatabaseAnswer& p = @@ -1346,8 +1608,7 @@ return true; } - - std::auto_ptr<boost::recursive_mutex::scoped_lock> lock; // (*) + boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_); // (*) switch (service) { @@ -1401,6 +1662,14 @@ RegisterOnChangeCallback(parameters); return true; + case _OrthancPluginService_RegisterWorklistCallback: + RegisterWorklistCallback(parameters); + return true; + + case _OrthancPluginService_RegisterDecodeImageCallback: + RegisterDecodeImageCallback(parameters); + return true; + case _OrthancPluginService_AnswerBuffer: AnswerBuffer(parameters); return true; @@ -1496,6 +1765,7 @@ case _OrthancPluginService_GetInstanceSimplifiedJson: case _OrthancPluginService_HasInstanceMetadata: case _OrthancPluginService_GetInstanceMetadata: + case _OrthancPluginService_GetInstanceOrigin: AccessDicomInstance(service, parameters); return true; @@ -1712,7 +1982,7 @@ case _OrthancPluginService_GetImageBuffer: { const _OrthancPluginGetImageInfo& p = *reinterpret_cast<const _OrthancPluginGetImageInfo*>(parameters); - *(p.resultBuffer) = reinterpret_cast<const ImageAccessor*>(p.image)->GetConstBuffer(); + *(p.resultBuffer) = reinterpret_cast<const ImageAccessor*>(p.image)->GetBuffer(); return true; } @@ -1831,6 +2101,53 @@ ApplyDicomToJson(service, parameters); return true; + case _OrthancPluginService_CreateDicom: + ApplyCreateDicom(service, parameters); + return true; + + case _OrthancPluginService_WorklistAddAnswer: + { + const _OrthancPluginWorklistAnswersOperation& p = + *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters); + reinterpret_cast<const WorklistHandler*>(p.query)->AddAnswer(p.answers, p.dicom, p.size); + return true; + } + + case _OrthancPluginService_WorklistMarkIncomplete: + { + const _OrthancPluginWorklistAnswersOperation& p = + *reinterpret_cast<const _OrthancPluginWorklistAnswersOperation*>(parameters); + reinterpret_cast<DicomFindAnswers*>(p.answers)->SetComplete(false); + return true; + } + + case _OrthancPluginService_WorklistIsMatch: + { + const _OrthancPluginWorklistQueryOperation& p = + *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); + *p.isMatch = reinterpret_cast<const WorklistHandler*>(p.query)->IsMatch(p.dicom, p.size); + return true; + } + + case _OrthancPluginService_WorklistGetDicomQuery: + { + const _OrthancPluginWorklistQueryOperation& p = + *reinterpret_cast<const _OrthancPluginWorklistQueryOperation*>(parameters); + reinterpret_cast<const WorklistHandler*>(p.query)->GetDicomQuery(*p.target); + return true; + } + + case _OrthancPluginService_CreateImage: + case _OrthancPluginService_CreateImageAccessor: + case _OrthancPluginService_DecodeDicomImage: + ApplyCreateImage(service, parameters); + return true; + + case _OrthancPluginService_ComputeMd5: + case _OrthancPluginService_ComputeSha1: + ComputeHash(service, parameters); + return true; + default: { // This service is unknown to the Orthanc plugin engine @@ -1948,4 +2265,50 @@ { return pimpl_->dictionary_; } + + + IWorklistRequestHandler* OrthancPlugins::ConstructWorklistRequestHandler() + { + if (HasWorklistHandler()) + { + return new WorklistHandler(*this); + } + else + { + return NULL; + } + } + + + bool OrthancPlugins::HasWorklistHandler() + { + boost::mutex::scoped_lock lock(pimpl_->worklistCallbackMutex_); + return pimpl_->worklistCallback_ != NULL; + } + + + ImageAccessor* OrthancPlugins::Decode(ParsedDicomFile& dicom, + unsigned int frame) + { + { + boost::mutex::scoped_lock lock(pimpl_->decodeImageCallbackMutex_); + if (pimpl_->decodeImageCallback_ != NULL) + { + std::string s; + dicom.SaveToMemoryBuffer(s); + + OrthancPluginImage* pluginImage = NULL; + if (pimpl_->decodeImageCallback_(&pluginImage, s.c_str(), s.size(), frame) == OrthancPluginErrorCode_Success && + pluginImage != NULL) + { + return reinterpret_cast<ImageAccessor*>(pluginImage); + } + + LOG(WARNING) << "The custom image decoder cannot handle an image, fallback to the built-in decoder"; + } + } + + DicomImageDecoder defaultDecoder; + return defaultDecoder.Decode(dicom, frame); + } }
--- a/Plugins/Engine/OrthancPlugins.h Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Engine/OrthancPlugins.h Wed Dec 02 09:52:56 2015 +0100 @@ -50,6 +50,8 @@ #include "../../Core/FileStorage/IStorageArea.h" #include "../../Core/HttpServer/IHttpHandler.h" #include "../../OrthancServer/IServerListener.h" +#include "../../OrthancServer/IDicomImageDecoder.h" +#include "../../OrthancServer/DicomProtocol/IWorklistRequestHandlerFactory.h" #include "OrthancPluginDatabase.h" #include "PluginsManager.h" @@ -63,12 +65,16 @@ class OrthancPlugins : public IHttpHandler, public IPluginServiceProvider, - public IServerListener + public IServerListener, + public IWorklistRequestHandlerFactory, + public IDicomImageDecoder { private: struct PImpl; boost::shared_ptr<PImpl> pimpl_; + class WorklistHandler; + void CheckContextAvailable(); void RegisterRestCallback(const void* parameters, @@ -78,6 +84,10 @@ void RegisterOnChangeCallback(const void* parameters); + void RegisterWorklistCallback(const void* parameters); + + void RegisterDecodeImageCallback(const void* parameters); + void AnswerBuffer(const void* parameters); void Redirect(const void* parameters); @@ -134,6 +144,15 @@ void ApplyDicomToJson(_OrthancPluginService service, const void* parameters); + void ApplyCreateDicom(_OrthancPluginService service, + const void* parameters); + + void ApplyCreateImage(_OrthancPluginService service, + const void* parameters); + + void ComputeHash(_OrthancPluginService service, + const void* parameters); + void SignalChangeInternal(OrthancPluginChangeType changeType, OrthancPluginResourceType resourceType, const char* resource); @@ -204,6 +223,13 @@ { SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL); } + + bool HasWorklistHandler(); + + virtual IWorklistRequestHandler* ConstructWorklistRequestHandler(); + + virtual ImageAccessor* Decode(ParsedDicomFile& dicom, + unsigned int frame); }; }
--- a/Plugins/Engine/PluginsEnumerations.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Engine/PluginsEnumerations.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -279,6 +279,31 @@ } + OrthancPluginInstanceOrigin Convert(RequestOrigin origin) + { + switch (origin) + { + case RequestOrigin_DicomProtocol: + return OrthancPluginInstanceOrigin_DicomProtocol; + + case RequestOrigin_RestApi: + return OrthancPluginInstanceOrigin_RestApi; + + case RequestOrigin_Lua: + return OrthancPluginInstanceOrigin_Lua; + + case RequestOrigin_Plugins: + return OrthancPluginInstanceOrigin_Plugin; + + case RequestOrigin_Unknown: + return OrthancPluginInstanceOrigin_Unknown; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + #if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0 DcmEVR Convert(OrthancPluginValueRepresentation vr) {
--- a/Plugins/Engine/PluginsEnumerations.h Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Engine/PluginsEnumerations.h Wed Dec 02 09:52:56 2015 +0100 @@ -65,6 +65,8 @@ IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint); + OrthancPluginInstanceOrigin Convert(RequestOrigin origin); + #if !defined(ORTHANC_ENABLE_DCMTK) || ORTHANC_ENABLE_DCMTK != 0 DcmEVR Convert(OrthancPluginValueRepresentation vr); #endif
--- a/Plugins/Include/orthanc/OrthancCPlugin.h Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Wed Dec 02 09:52:56 2015 +0100 @@ -18,6 +18,8 @@ * - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback(). * - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea(). * - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV2(). + * - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback(). + * - Possibly register a custom decoder for DICOM images using OrthancPluginRegisterDecodeImageCallback(). * -# <tt>void OrthancPluginFinalize()</tt>: * This function is invoked by Orthanc during its shutdown. The plugin * must free all its memory. @@ -49,6 +51,9 @@ * @defgroup Callbacks Callbacks * @brief Functions to register and manage callbacks by the plugins. * + * @defgroup Worklists Worklists + * @brief Functions to register and manage worklists. + * * @defgroup Orthanc Orthanc * @brief Functions to access the content of the Orthanc server. **/ @@ -271,6 +276,7 @@ OrthancPluginErrorCode_DatabaseNotInitialized = 2038 /*!< Plugin trying to call the database during its initialization */, OrthancPluginErrorCode_SslDisabled = 2039 /*!< Orthanc has been built without SSL support */, OrthancPluginErrorCode_CannotOrderSlices = 2040 /*!< Unable to order the slices of the series */, + OrthancPluginErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, _OrthancPluginErrorCode_INTERNAL = 0x7fffffff } OrthancPluginErrorCode; @@ -391,6 +397,9 @@ _OrthancPluginService_RegisterDictionaryTag = 20, _OrthancPluginService_DicomBufferToJson = 21, _OrthancPluginService_DicomInstanceToJson = 22, + _OrthancPluginService_CreateDicom = 23, + _OrthancPluginService_ComputeMd5 = 24, + _OrthancPluginService_ComputeSha1 = 25, /* Registration of callbacks */ _OrthancPluginService_RegisterRestCallback = 1000, @@ -398,6 +407,8 @@ _OrthancPluginService_RegisterStorageArea = 1002, _OrthancPluginService_RegisterOnChangeCallback = 1003, _OrthancPluginService_RegisterRestCallbackNoLock = 1004, + _OrthancPluginService_RegisterWorklistCallback = 1005, + _OrthancPluginService_RegisterDecodeImageCallback = 1006, /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -439,6 +450,7 @@ _OrthancPluginService_GetInstanceSimplifiedJson = 4004, _OrthancPluginService_HasInstanceMetadata = 4005, _OrthancPluginService_GetInstanceMetadata = 4006, + _OrthancPluginService_GetInstanceOrigin = 4007, /* Services for plugins implementing a database back-end */ _OrthancPluginService_RegisterDatabaseBackend = 5000, @@ -461,6 +473,15 @@ _OrthancPluginService_GetFontsCount = 6009, _OrthancPluginService_GetFontInfo = 6010, _OrthancPluginService_DrawText = 6011, + _OrthancPluginService_CreateImage = 6012, + _OrthancPluginService_CreateImageAccessor = 6013, + _OrthancPluginService_DecodeDicomImage = 6014, + + /* Primitives for handling worklists */ + _OrthancPluginService_WorklistAddAnswer = 7000, + _OrthancPluginService_WorklistMarkIncomplete = 7001, + _OrthancPluginService_WorklistIsMatch = 7002, + _OrthancPluginService_WorklistGetDicomQuery = 7003, _OrthancPluginService_INTERNAL = 0x7fffffff } _OrthancPluginService; @@ -606,8 +627,9 @@ **/ typedef enum { - OrthancPluginImageFormat_Png = 0, /*!< Image compressed using PNG */ - OrthancPluginImageFormat_Jpeg = 1, /*!< Image compressed using JPEG */ + OrthancPluginImageFormat_Png = 0, /*!< Image compressed using PNG */ + OrthancPluginImageFormat_Jpeg = 1, /*!< Image compressed using JPEG */ + OrthancPluginImageFormat_Dicom = 2, /*!< Image compressed using DICOM */ _OrthancPluginImageFormat_INTERNAL = 0x7fffffff } OrthancPluginImageFormat; @@ -654,6 +676,7 @@ /** * The possible output formats for a DICOM-to-JSON conversion. * @ingroup Toolbox + * @see OrthancPluginDicomToJson() **/ typedef enum { @@ -684,20 +707,48 @@ /** + * Flags to the creation of a DICOM file. + * @ingroup Toolbox + * @see OrthancPluginCreateDicom() + **/ + typedef enum + { + OrthancPluginCreateDicomFlags_DecodeDataUriScheme = (1 << 0), /*!< Decode fields encoded using data URI scheme */ + OrthancPluginCreateDicomFlags_GenerateIdentifiers = (1 << 1), /*!< Automatically generate DICOM identifiers */ + + _OrthancPluginCreateDicomFlags_INTERNAL = 0x7fffffff + } OrthancPluginCreateDicomFlags; + + + /** * The constraints on the DICOM identifiers that must be supported * by the database plugins. **/ typedef enum { - OrthancPluginIdentifierConstraint_Equal, /*!< Equal */ - OrthancPluginIdentifierConstraint_SmallerOrEqual, /*!< Less or equal */ - OrthancPluginIdentifierConstraint_GreaterOrEqual, /*!< More or equal */ - OrthancPluginIdentifierConstraint_Wildcard, /*!< Case-sensitive wildcard matching (with * and ?) */ + OrthancPluginIdentifierConstraint_Equal = 1, /*!< Equal */ + OrthancPluginIdentifierConstraint_SmallerOrEqual = 2, /*!< Less or equal */ + OrthancPluginIdentifierConstraint_GreaterOrEqual = 3, /*!< More or equal */ + OrthancPluginIdentifierConstraint_Wildcard = 4, /*!< Case-sensitive wildcard matching (with * and ?) */ _OrthancPluginIdentifierConstraint_INTERNAL = 0x7fffffff } OrthancPluginIdentifierConstraint; + /** + * The origin of a DICOM instance that has been received by Orthanc. + **/ + typedef enum + { + OrthancPluginInstanceOrigin_Unknown = 1, /*!< Unknown origin */ + OrthancPluginInstanceOrigin_DicomProtocol = 2, /*!< Instance received through DICOM protocol */ + OrthancPluginInstanceOrigin_RestApi = 3, /*!< Instance received through REST API of Orthanc */ + OrthancPluginInstanceOrigin_Plugin = 4, /*!< Instance added to Orthanc by a plugin */ + OrthancPluginInstanceOrigin_Lua = 5, /*!< Instance added to Orthanc by a Lua script */ + + _OrthancPluginInstanceOrigin_INTERNAL = 0x7fffffff + } OrthancPluginInstanceOrigin; + /** * @brief A memory buffer allocated by the core system of Orthanc. @@ -754,6 +805,22 @@ /** + * @brief Opaque structure to an object that represents a C-Find query. + * @ingroup Worklists + **/ + typedef struct _OrthancPluginWorklistQuery_t OrthancPluginWorklistQuery; + + + + /** + * @brief Opaque structure to an object that represents the answers to a C-Find query. + * @ingroup Worklists + **/ + typedef struct _OrthancPluginWorklistAnswers_t OrthancPluginWorklistAnswers; + + + + /** * @brief Signature of a callback function that answers to a REST request. * @ingroup Callbacks **/ @@ -786,6 +853,18 @@ /** + * @brief Signature of a callback function to decode a DICOM instance as an image. + * @ingroup Callbacks + **/ + typedef OrthancPluginErrorCode (*OrthancPluginDecodeImageCallback) ( + OrthancPluginImage** target, + const void* dicom, + const uint32_t size, + uint32_t frameIndex); + + + + /** * @brief Signature of a function to free dynamic memory. **/ typedef void (*OrthancPluginFree) (void* buffer); @@ -849,6 +928,27 @@ /** + * @brief Callback to handle the C-Find SCP requests received by Orthanc. + * + * Signature of a callback function that is triggered when Orthanc + * receives a C-Find SCP request against modality worklists. + * + * @param answers The target structure where answers must be stored. + * @param query The worklist query. + * @param remoteAet The Application Entity Title (AET) of the modality from which the request originates. + * @param calledAet The Application Entity Title (AET) of the modality that is called by the request. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + typedef OrthancPluginErrorCode (*OrthancPluginWorklistCallback) ( + OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* remoteAet, + const char* calledAet); + + + + /** * @brief Data structure that contains information about the Orthanc core. **/ typedef struct _OrthancPluginContext_t @@ -913,7 +1013,9 @@ sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) || sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) || - sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint)) + sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) || + sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) || + sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin)) { /* Mismatch in the size of the enumerations */ return 0; @@ -1248,7 +1350,7 @@ * file is stored into a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param instanceId The Orthanc identifier of the DICOM instance of interest. * @return 0 if success, or the error code if failure. * @ingroup Orthanc @@ -1279,7 +1381,7 @@ * the query is stored into a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param uri The URI in the built-in Orthanc API. * @return 0 if success, or the error code if failure. * @see OrthancPluginRestApiGetAfterPlugins @@ -1308,7 +1410,7 @@ * query is stored into a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param uri The URI in the built-in Orthanc API. * @return 0 if success, or the error code if failure. * @see OrthancPluginRestApiGet @@ -1342,7 +1444,7 @@ * the query is stored into a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param uri The URI in the built-in Orthanc API. * @param body The body of the POST request. * @param bodySize The size of the body. @@ -1376,7 +1478,7 @@ * query is stored into a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param uri The URI in the built-in Orthanc API. * @param body The body of the POST request. * @param bodySize The size of the body. @@ -1450,7 +1552,7 @@ * the query is stored into a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param uri The URI in the built-in Orthanc API. * @param body The body of the PUT request. * @param bodySize The size of the body. @@ -1485,7 +1587,7 @@ * query is stored into a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param uri The URI in the built-in Orthanc API. * @param body The body of the PUT request. * @param bodySize The size of the body. @@ -1861,11 +1963,12 @@ typedef struct { - char** resultStringToFree; - const char** resultString; - int64_t* resultInt64; - const char* key; - OrthancPluginDicomInstance* instance; + char** resultStringToFree; + const char** resultString; + int64_t* resultInt64; + const char* key; + OrthancPluginDicomInstance* instance; + OrthancPluginInstanceOrigin* resultOrigin; /* New in Orthanc 0.9.5 SDK */ } _OrthancPluginAccessDicomInstance; @@ -2665,7 +2768,7 @@ * version of the zlib library that is used by the Orthanc core. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param source The source buffer. * @param size The size in bytes of the source buffer. * @param compression The compression algorithm. @@ -2707,7 +2810,7 @@ * a newly allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param path The path of the file to be read. * @return 0 if success, or the error code if failure. **/ @@ -2848,7 +2951,7 @@ const OrthancPluginImage* image; uint32_t* resultUint32; OrthancPluginPixelFormat* resultPixelFormat; - const void** resultBuffer; + void** resultBuffer; } _OrthancPluginGetImageInfo; @@ -2997,11 +3100,11 @@ * @return The pointer. * @ingroup Images **/ - ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetImageBuffer( + ORTHANC_PLUGIN_INLINE void* OrthancPluginGetImageBuffer( OrthancPluginContext* context, const OrthancPluginImage* image) { - const void* target = NULL; + void* target = NULL; _OrthancPluginGetImageInfo params; memset(¶ms, 0, sizeof(params)); @@ -3265,7 +3368,7 @@ * Orthanc instance that hosts this plugin. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param url The URL of interest. * @param username The username (can be <tt>NULL</tt> if no password protection). * @param password The password (can be <tt>NULL</tt> if no password protection). @@ -3300,7 +3403,7 @@ * the Orthanc instance that hosts this plugin. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param url The URL of interest. * @param body The content of the body of the request. * @param bodySize The size of the body of the request. @@ -3341,7 +3444,7 @@ * Orthanc instance that hosts this plugin. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param url The URL of interest. * @param body The content of the body of the request. * @param bodySize The size of the body of the request. @@ -3890,7 +3993,7 @@ * @param buffer The memory buffer containing the DICOM file. * @param size The size of the memory buffer. * @param format The output format. - * @param flags The output flags. + * @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 @@ -3939,7 +4042,7 @@ * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). * @param instanceId The Orthanc identifier of the instance. * @param format The output format. - * @param flags The output flags. + * @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 @@ -3994,7 +4097,7 @@ * allocated memory buffer. * * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). - * @param target The target memory buffer. + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). * @param uri The URI in the built-in Orthanc API. * @param headersCount The number of HTTP headers. * @param headersKeys Array containing the keys of the HTTP headers. @@ -4025,6 +4128,505 @@ return context->InvokeService(context, _OrthancPluginService_RestApiGet2, ¶ms); } + + + typedef struct + { + OrthancPluginWorklistCallback callback; + } _OrthancPluginWorklistCallback; + + /** + * @brief Register a callback to handle modality worklists requests. + * + * This function registers a callback to handle C-Find SCP requests + * on modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWorklistCallback( + OrthancPluginContext* context, + OrthancPluginWorklistCallback callback) + { + _OrthancPluginWorklistCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterWorklistCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginWorklistAnswers* answers; + const OrthancPluginWorklistQuery* query; + const void* dicom; + uint32_t size; + } _OrthancPluginWorklistAnswersOperation; + + /** + * @brief Add one answer to some modality worklist request. + * + * This function adds one worklist (encoded as a DICOM file) to the + * set of answers corresponding to some C-Find SCP request against + * modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @param query The worklist query, as received by the callback. + * @param dicom The worklist to answer, encoded as a DICOM file. + * @param size The size of the DICOM file. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWorklistAddAnswer( + OrthancPluginContext* context, + OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const void* dicom, + uint32_t size) + { + _OrthancPluginWorklistAnswersOperation params; + params.answers = answers; + params.query = query; + params.dicom = dicom; + params.size = size; + + return context->InvokeService(context, _OrthancPluginService_WorklistAddAnswer, ¶ms); + } + + + /** + * @brief Mark the set of worklist answers as incomplete. + * + * This function marks as incomplete the set of answers + * corresponding to some C-Find SCP request against modality + * worklists. This must be used if canceling the handling of a + * request when too many answers are to be returned. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param answers The set of answers. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWorklistMarkIncomplete( + OrthancPluginContext* context, + OrthancPluginWorklistAnswers* answers) + { + _OrthancPluginWorklistAnswersOperation params; + params.answers = answers; + params.query = NULL; + params.dicom = NULL; + params.size = 0; + + return context->InvokeService(context, _OrthancPluginService_WorklistMarkIncomplete, ¶ms); + } + + + typedef struct + { + const OrthancPluginWorklistQuery* query; + const void* dicom; + uint32_t size; + int32_t* isMatch; + OrthancPluginMemoryBuffer* target; + } _OrthancPluginWorklistQueryOperation; + + /** + * @brief Test whether a worklist matches the query. + * + * This function checks whether one worklist (encoded as a DICOM + * file) matches the C-Find SCP query against modality + * worklists. This function must be called before adding the + * worklist as an answer through OrthancPluginWorklistAddAnswer(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param query The worklist query, as received by the callback. + * @param dicom The worklist to answer, encoded as a DICOM file. + * @param size The size of the DICOM file. + * @return 1 if the worklist matches the query, 0 otherwise. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginWorklistIsMatch( + OrthancPluginContext* context, + const OrthancPluginWorklistQuery* query, + const void* dicom, + uint32_t size) + { + int32_t isMatch = 0; + + _OrthancPluginWorklistQueryOperation params; + params.query = query; + params.dicom = dicom; + params.size = size; + params.isMatch = &isMatch; + params.target = NULL; + + if (context->InvokeService(context, _OrthancPluginService_WorklistIsMatch, ¶ms) == OrthancPluginErrorCode_Success) + { + return isMatch; + } + else + { + /* Error: Assume non-match */ + return 0; + } + } + + + /** + * @brief Retrieve the worklist query as a DICOM file. + * + * This function retrieves the DICOM file that underlies a C-Find + * SCP query against modality worklists. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param query The worklist query, as received by the callback. + * @return 0 if success, other value if error. + * @ingroup Worklists + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginWorklistGetDicomQuery( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const OrthancPluginWorklistQuery* query) + { + _OrthancPluginWorklistQueryOperation params; + params.query = query; + params.dicom = NULL; + params.size = 0; + params.isMatch = NULL; + params.target = target; + + return context->InvokeService(context, _OrthancPluginService_WorklistGetDicomQuery, ¶ms); + } + + + /** + * @brief Get the origin of a DICOM file. + * + * This function returns the origin of a DICOM instance that has been received by Orthanc. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instance The instance of interest. + * @return The origin of the instance. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin( + OrthancPluginContext* context, + OrthancPluginDicomInstance* instance) + { + OrthancPluginInstanceOrigin origin; + + _OrthancPluginAccessDicomInstance params; + memset(¶ms, 0, sizeof(params)); + params.resultOrigin = &origin; + params.instance = instance; + + if (context->InvokeService(context, _OrthancPluginService_GetInstanceOrigin, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return OrthancPluginInstanceOrigin_Unknown; + } + else + { + return origin; + } + } + + + typedef struct + { + OrthancPluginMemoryBuffer* target; + const char* json; + const OrthancPluginImage* pixelData; + OrthancPluginCreateDicomFlags flags; + } _OrthancPluginCreateDicom; + + /** + * @brief Create a DICOM instance from a JSON string and an image. + * + * This function takes as input a string containing a JSON file + * describing the content of a DICOM instance. As an output, it + * writes the corresponding DICOM instance to a newly allocated + * memory buffer. Additionally, an image to be encoded within the + * DICOM instance can also be provided. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer(). + * @param json The input JSON file. + * @param pixelData The image. Can be NULL, if the pixel data is encoded inside the JSON with the data URI scheme. + * @param flags Flags governing the output. + * @return 0 if success, other value if error. + * @ingroup Toolbox + * @see OrthancPluginDicomBufferToJson + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateDicom( + OrthancPluginContext* context, + OrthancPluginMemoryBuffer* target, + const char* json, + const OrthancPluginImage* pixelData, + OrthancPluginCreateDicomFlags flags) + { + _OrthancPluginCreateDicom params; + params.target = target; + params.json = json; + params.pixelData = pixelData; + params.flags = flags; + + return context->InvokeService(context, _OrthancPluginService_CreateDicom, ¶ms); + } + + + typedef struct + { + OrthancPluginDecodeImageCallback callback; + } _OrthancPluginDecodeImageCallback; + + /** + * @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. + * + * @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 OrthancPluginRegisterDecodeImageCallback( + OrthancPluginContext* context, + OrthancPluginDecodeImageCallback callback) + { + _OrthancPluginDecodeImageCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterDecodeImageCallback, ¶ms); + } + + + + typedef struct + { + OrthancPluginImage** target; + OrthancPluginPixelFormat format; + uint32_t width; + uint32_t height; + uint32_t pitch; + void* buffer; + const void* constBuffer; + uint32_t bufferSize; + uint32_t frameIndex; + } _OrthancPluginCreateImage; + + + /** + * @brief Create an image. + * + * This function creates an image of given size and format. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param format The format of the pixels. + * @param width The width of the image. + * @param height The height of the image. + * @return The newly allocated image. It must be freed with OrthancPluginFreeImage(). + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImage( + OrthancPluginContext* context, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginCreateImage params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.format = format; + params.width = width; + params.height = height; + + if (context->InvokeService(context, _OrthancPluginService_CreateImage, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + /** + * @brief Create an image pointing to a memory buffer. + * + * This function creates an image whose content points to a memory + * buffer managed by the plugin. Note that the buffer is directly + * accessed, no memory is allocated and no data is copied. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param format The format of the pixels. + * @param width The width of the image. + * @param height The height of the image. + * @param pitch The pitch of the image (i.e. the number of bytes + * between 2 successive lines of the image in the memory buffer). + * @param buffer The memory buffer. + * @return The newly allocated image. It must be freed with OrthancPluginFreeImage(). + * @ingroup Images + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginCreateImageAccessor( + OrthancPluginContext* context, + OrthancPluginPixelFormat format, + uint32_t width, + uint32_t height, + uint32_t pitch, + void* buffer) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginCreateImage params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.format = format; + params.width = width; + params.height = height; + params.pitch = pitch; + params.buffer = buffer; + + if (context->InvokeService(context, _OrthancPluginService_CreateImageAccessor, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + + /** + * @brief Decode one frame from a DICOM instance. + * + * This function decodes one frame of a DICOM image that is stored + * in a memory buffer. This function will give the same result as + * OrthancPluginUncompressImage() for single-frame DICOM images. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer Pointer to a memory buffer containing the DICOM image. + * @param bufferSize Size of the memory buffer containing the DICOM image. + * @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 + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginDecodeDicomImage( + OrthancPluginContext* context, + const void* buffer, + uint32_t bufferSize, + uint32_t frameIndex) + { + OrthancPluginImage* target = NULL; + + _OrthancPluginCreateImage params; + memset(¶ms, 0, sizeof(params)); + params.target = ⌖ + params.constBuffer = buffer; + params.bufferSize = bufferSize; + params.frameIndex = frameIndex; + + if (context->InvokeService(context, _OrthancPluginService_DecodeDicomImage, ¶ms) != OrthancPluginErrorCode_Success) + { + return NULL; + } + else + { + return target; + } + } + + + + typedef struct + { + char** result; + const void* buffer; + uint32_t size; + } _OrthancPluginComputeHash; + + /** + * @brief Compute an MD5 hash. + * + * This functions computes the MD5 cryptographic hash of the given memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The source memory buffer. + * @param size The size in bytes of the source buffer. + * @return The NULL value in case of error, or a string containing the cryptographic hash. + * This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeMd5( + OrthancPluginContext* context, + const void* buffer, + uint32_t size) + { + char* result; + + _OrthancPluginComputeHash params; + params.result = &result; + params.buffer = buffer; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_ComputeMd5, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + + + /** + * @brief Compute a SHA-1 hash. + * + * This functions computes the SHA-1 cryptographic hash of the given memory buffer. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param buffer The source memory buffer. + * @param size The size in bytes of the source buffer. + * @return The NULL value in case of error, or a string containing the cryptographic hash. + * This string must be freed by OrthancPluginFreeString(). + * @ingroup Toolbox + **/ + ORTHANC_PLUGIN_INLINE char* OrthancPluginComputeSha1( + OrthancPluginContext* context, + const void* buffer, + uint32_t size) + { + char* result; + + _OrthancPluginComputeHash params; + params.result = &result; + params.buffer = buffer; + params.size = size; + + if (context->InvokeService(context, _OrthancPluginService_ComputeSha1, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return result; + } + } + #ifdef __cplusplus } #endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 2.8) + +project(Basic) + +set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) +include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake) + +add_library(AutomatedJpeg2kCompression SHARED Plugin.cpp)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,162 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include <orthanc/OrthancCPlugin.h> + +#include <string> + +static OrthancPluginContext* context_ = NULL; + + +static bool ReadFile(std::string& result, + const std::string& path) +{ + OrthancPluginMemoryBuffer tmp; + if (OrthancPluginReadFile(context_, &tmp, path.c_str()) == OrthancPluginErrorCode_Success) + { + result.assign(reinterpret_cast<const char*>(tmp.data), tmp.size); + OrthancPluginFreeMemoryBuffer(context_, &tmp); + return true; + } + else + { + return false; + } +} + + +OrthancPluginErrorCode OnStoredCallback(OrthancPluginDicomInstance* instance, + const char* instanceId) +{ + char buffer[1024]; + sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from origin %d (AET %s)", + (int) OrthancPluginGetInstanceSize(context_, instance), instanceId, + OrthancPluginGetInstanceOrigin(context_, instance), + OrthancPluginGetInstanceRemoteAet(context_, instance)); + OrthancPluginLogInfo(context_, buffer); + + if (OrthancPluginGetInstanceOrigin(context_, instance) == OrthancPluginInstanceOrigin_Plugin) + { + // Do not compress twice the same file + return OrthancPluginErrorCode_Success; + } + + // Write the uncompressed DICOM content to some temporary file + std::string uncompressed = "uncompressed-" + std::string(instanceId) + ".dcm"; + OrthancPluginErrorCode error = OrthancPluginWriteFile(context_, uncompressed.c_str(), + OrthancPluginGetInstanceData(context_, instance), + OrthancPluginGetInstanceSize(context_, instance)); + if (error) + { + return error; + } + + // Remove the original DICOM instance + std::string uri = "/instances/" + std::string(instanceId); + error = OrthancPluginRestApiDelete(context_, uri.c_str()); + if (error) + { + return error; + } + + // Path to the temporary file that will contain the compressed DICOM content + std::string compressed = "compressed-" + std::string(instanceId) + ".dcm"; + + // Compress to JPEG2000 using gdcm + std::string command1 = "gdcmconv --j2k " + uncompressed + " " + compressed; + + // Generate a new SOPInstanceUID for the JPEG2000 file, as gdcmconv + // does not do this by itself + std::string command2 = "dcmodify --no-backup -gin " + compressed; + + // Make the required system calls + system(command1.c_str()); + system(command2.c_str()); + + // Read the result of the JPEG2000 compression + std::string j2k; + bool ok = ReadFile(j2k, compressed); + + // Remove the two temporary files + remove(compressed.c_str()); + remove(uncompressed.c_str()); + + if (!ok) + { + return OrthancPluginErrorCode_Plugin; + } + + // Upload the JPEG2000 file through the REST API + OrthancPluginMemoryBuffer tmp; + if (OrthancPluginRestApiPost(context_, &tmp, "/instances", j2k.c_str(), j2k.size())) + { + ok = false; + } + + if (ok) + { + OrthancPluginFreeMemoryBuffer(context_, &tmp); + } + + return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_Plugin; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + context_ = c; + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 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; + } + + OrthancPluginRegisterOnStoredInstanceCallback(context_, OnStoredCallback); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "sample-jpeg2k"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "0.0"; + } +}
--- a/Plugins/Samples/Basic/Plugin.c Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Samples/Basic/Plugin.c Wed Dec 02 09:52:56 2015 +0100 @@ -262,8 +262,9 @@ char* json; static int first = 1; - sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from AET %s", + sprintf(buffer, "Just received a DICOM instance of size %d and ID %s from origin %d (AET %s)", (int) OrthancPluginGetInstanceSize(context, instance), instanceId, + OrthancPluginGetInstanceOrigin(context, instance), OrthancPluginGetInstanceRemoteAet(context, instance)); OrthancPluginLogWarning(context, buffer);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/CustomImageDecoder/CMakeLists.txt Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 2.8) + +project(CustomImageDecoder) + +set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) +include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake) + +add_library(PluginTest SHARED Plugin.cpp)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/CustomImageDecoder/Plugin.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,80 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include <orthanc/OrthancCPlugin.h> + +static OrthancPluginContext* context_ = NULL; + + +static OrthancPluginErrorCode DecodeImageCallback(OrthancPluginImage** target, + const void* dicom, + const uint32_t size, + uint32_t frameIndex) +{ + *target = OrthancPluginCreateImage(context_, OrthancPluginPixelFormat_RGB24, 512, 512); + + memset(OrthancPluginGetImageBuffer(context_, *target), 128, + OrthancPluginGetImageHeight(context_, *target) * OrthancPluginGetImagePitch(context_, *target)); + + return OrthancPluginDrawText(context_, *target, 0, "Hello world", 100, 50, 255, 0, 0); +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + context_ = c; + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 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; + } + + OrthancPluginRegisterDecodeImageCallback(context_, DecodeImageCallback); + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "custom-image-decoder"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return "0.0"; + } +}
--- a/Plugins/Samples/DatabasePlugin/Database.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Samples/DatabasePlugin/Database.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -123,7 +123,8 @@ public: SignalRemainingAncestor() : - hasRemainingAncestor_(false) + hasRemainingAncestor_(false), + remainingType_(OrthancPluginResourceType_Instance) // Some dummy value { } @@ -175,7 +176,8 @@ Database::Database(const std::string& path) : path_(path), - base_(db_) + base_(db_), + signalRemainingAncestor_(NULL) { } @@ -381,7 +383,7 @@ const std::list<I>& source) { for (typename std::list<I>::const_iterator - it = source.begin(); it != source.end(); it++) + it = source.begin(); it != source.end(); ++it) { target.push_back(*it); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/CMakeLists.txt Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,32 @@ +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})
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,98 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "GdcmDecoderCache.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::auto_ptr<GdcmImageDecoder> decoder(new GdcmImageDecoder(dicom, size)); + std::auto_ptr<OrthancImageWrapper> image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex))); + + { + // Cache the newly created decoder for further use + boost::mutex::scoped_lock lock(mutex_); + decoder_ = decoder; + size_ = size; + md5_ = md5; + } + + return image.release(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "GdcmImageDecoder.h" +#include "OrthancImageWrapper.h" + +#include <boost/thread.hpp> + + +namespace OrthancPlugins +{ + class GdcmDecoderCache : public boost::noncopyable + { + private: + boost::mutex mutex_; + std::auto_ptr<OrthancPlugins::GdcmImageDecoder> 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); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,300 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "GdcmImageDecoder.h" + +#include "OrthancImageWrapper.h" + +#include <gdcmImageReader.h> +#include <gdcmImageApplyLookupTable.h> +#include <gdcmImageChangePlanarConfiguration.h> +#include <gdcmImageChangePhotometricInterpretation.h> +#include <stdexcept> +#include <boost/iostreams/stream.hpp> +#include <boost/iostreams/device/array.hpp> + + +namespace OrthancPlugins +{ + struct GdcmImageDecoder::PImpl + { + const void* dicom_; + size_t size_; + + gdcm::ImageReader reader_; + std::auto_ptr<gdcm::ImageApplyLookupTable> lut_; + std::auto_ptr<gdcm::ImageChangePhotometricInterpretation> photometric_; + std::auto_ptr<gdcm::ImageChangePlanarConfiguration> 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) + { + 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<char> source(reinterpret_cast<const char*>(dicom), size); + stream<basic_array_source<char> > 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) + { + switch (image.GetPixelFormat()) + { + case gdcm::PixelFormat::UINT8: + return OrthancPluginPixelFormat_RGB24; + + 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; + + default: + throw std::runtime_error("Unsupport pixel format"); + } + } + + + 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()); + pimpl_->GetImage().GetBuffer(&decoded[0]); + } + + 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; + } + } + + return target.Release(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.h Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,53 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <orthanc/OrthancCPlugin.h> +#include <stdint.h> +#include <boost/noncopyable.hpp> +#include <boost/shared_ptr.hpp> + +namespace OrthancPlugins +{ + class GdcmImageDecoder : public boost::noncopyable + { + private: + struct PImpl; + boost::shared_ptr<PImpl> 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; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,99 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "OrthancImageWrapper.h" + +#include <stdexcept> + +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<char*>(OrthancPluginGetImageBuffer(context_, image_)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/OrthancImageWrapper.h Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,63 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include <orthanc/OrthancCPlugin.h> + +#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(); + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/Plugin.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,106 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include "GdcmDecoderCache.h" +#include "OrthancImageWrapper.h" + +#include <orthanc/OrthancCPlugin.h> + +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::auto_ptr<OrthancPlugins::OrthancImageWrapper> 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()); + OrthancPluginLogError(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; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/GdcmDecoder/README Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,6 @@ +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
--- a/Plugins/Samples/GdcmDecoding/CMakeLists.txt Wed Nov 18 10:16:21 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -cmake_minimum_required(VERSION 2.8) - -project(GdcmDecoding) - -SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") - -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of Boost") -SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") - -set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..) -include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) - -include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) -include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.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_library(GdcmDecoding SHARED - Plugin.cpp - OrthancContext.cpp - - # Sources from Orthanc - ${GOOGLE_LOG_SOURCES} - ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp - ${ORTHANC_ROOT}/Core/Enumerations.cpp - ${ORTHANC_ROOT}/Core/Images/ImageAccessor.cpp - ${ORTHANC_ROOT}/Core/Images/ImageBuffer.cpp - ${ORTHANC_ROOT}/Core/Images/ImageProcessing.cpp - ${ORTHANC_ROOT}/Core/Toolbox.cpp - ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp - ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c - ${JSONCPP_SOURCES} - ${THIRD_PARTY_SOURCES} - ) - -target_link_libraries(GdcmDecoding ${GDCM_LIBRARIES})
--- a/Plugins/Samples/GdcmDecoding/OrthancContext.cpp Wed Nov 18 10:16:21 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,151 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. - **/ - - -#include "OrthancContext.h" - -#include <stdexcept> - - -void OrthancContext::Check() -{ - if (context_ == NULL) - { - throw std::runtime_error("The Orthanc plugin context is not initialized"); - } -} - - -OrthancContext& OrthancContext::GetInstance() -{ - static OrthancContext instance; - return instance; -} - - -void OrthancContext::ExtractGetArguments(Arguments& arguments, - const OrthancPluginHttpRequest& request) -{ - Check(); - arguments.clear(); - - for (uint32_t i = 0; i < request.getCount; i++) - { - arguments[request.getKeys[i]] = request.getValues[i]; - } -} - - -void OrthancContext::LogError(const std::string& s) -{ - Check(); - OrthancPluginLogError(context_, s.c_str()); -} - - -void OrthancContext::LogWarning(const std::string& s) -{ - Check(); - OrthancPluginLogWarning(context_, s.c_str()); -} - - -void OrthancContext::LogInfo(const std::string& s) -{ - Check(); - OrthancPluginLogInfo(context_, s.c_str()); -} - - -void OrthancContext::Register(const std::string& uri, - OrthancPluginRestCallback callback) -{ - Check(); - OrthancPluginRegisterRestCallback(context_, uri.c_str(), callback); -} - - -void OrthancContext::GetDicomForInstance(std::string& result, - const std::string& instanceId) -{ - Check(); - OrthancPluginMemoryBuffer buffer; - - if (OrthancPluginGetDicomForInstance(context_, &buffer, instanceId.c_str())) - { - throw std::runtime_error("No DICOM instance with Orthanc ID: " + instanceId); - } - - if (buffer.size == 0) - { - result.clear(); - } - else - { - result.assign(reinterpret_cast<char*>(buffer.data), buffer.size); - } - - OrthancPluginFreeMemoryBuffer(context_, &buffer); -} - - -void OrthancContext::CompressAndAnswerPngImage(OrthancPluginRestOutput* output, - const Orthanc::ImageAccessor& accessor) -{ - Check(); - - OrthancPluginPixelFormat format; - switch (accessor.GetFormat()) - { - case Orthanc::PixelFormat_Grayscale8: - format = OrthancPluginPixelFormat_Grayscale8; - break; - - case Orthanc::PixelFormat_Grayscale16: - format = OrthancPluginPixelFormat_Grayscale16; - break; - - case Orthanc::PixelFormat_SignedGrayscale16: - format = OrthancPluginPixelFormat_SignedGrayscale16; - break; - - case Orthanc::PixelFormat_RGB24: - format = OrthancPluginPixelFormat_RGB24; - break; - - case Orthanc::PixelFormat_RGBA32: - format = OrthancPluginPixelFormat_RGBA32; - break; - - default: - throw std::runtime_error("Unsupported pixel format"); - } - - OrthancPluginCompressAndAnswerPngImage(context_, output, format, accessor.GetWidth(), - accessor.GetHeight(), accessor.GetPitch(), accessor.GetConstBuffer()); -} - - - -void OrthancContext::Redirect(OrthancPluginRestOutput* output, - const std::string& s) -{ - Check(); - OrthancPluginRedirect(context_, output, s.c_str()); -}
--- a/Plugins/Samples/GdcmDecoding/OrthancContext.h Wed Nov 18 10:16:21 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,78 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. - **/ - - -#pragma once - -#include <orthanc/OrthancCPlugin.h> - -#include "../../../Core/Images/ImageBuffer.h" - -#include <map> -#include <string> -#include <boost/noncopyable.hpp> - - -class OrthancContext : public boost::noncopyable -{ -private: - OrthancPluginContext* context_; - - OrthancContext() : context_(NULL) - { - } - - void Check(); - -public: - typedef std::map<std::string, std::string> Arguments; - - static OrthancContext& GetInstance(); - - void Initialize(OrthancPluginContext* context) - { - context_ = context; - } - - void Finalize() - { - context_ = NULL; - } - - void ExtractGetArguments(Arguments& arguments, - const OrthancPluginHttpRequest& request); - - void LogError(const std::string& s); - - void LogWarning(const std::string& s); - - void LogInfo(const std::string& s); - - void Register(const std::string& uri, - OrthancPluginRestCallback callback); - - void GetDicomForInstance(std::string& result, - const std::string& instanceId); - - void CompressAndAnswerPngImage(OrthancPluginRestOutput* output, - const Orthanc::ImageAccessor& accessor); - - void Redirect(OrthancPluginRestOutput* output, - const std::string& s); -};
--- a/Plugins/Samples/GdcmDecoding/Plugin.cpp Wed Nov 18 10:16:21 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,264 +0,0 @@ -/** - * Orthanc - A Lightweight, RESTful DICOM Store - * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. - **/ - - -#include <map> -#include <string> -#include <stdexcept> -#include <sstream> -#include <boost/lexical_cast.hpp> - -#include "OrthancContext.h" -#include "../../../Core/Images/ImageProcessing.h" - -#include <gdcmReader.h> -#include <gdcmImageReader.h> -#include <gdcmImageChangePlanarConfiguration.h> - - -static void AnswerUnsupportedImage(OrthancPluginRestOutput* output) -{ - OrthancContext::GetInstance().Redirect(output, "/app/images/unsupported.png"); -} - - -static bool GetOrthancPixelFormat(Orthanc::PixelFormat& format, - const gdcm::Image& image) -{ - if (image.GetPlanarConfiguration() != 0 && - image.GetPixelFormat().GetSamplesPerPixel() != 1) - { - OrthancContext::GetInstance().LogError("Planar configurations are not supported"); - return false; - } - - if (image.GetPixelFormat().GetSamplesPerPixel() == 1) - { - switch (image.GetPixelFormat().GetScalarType()) - { - case gdcm::PixelFormat::UINT8: - format = Orthanc::PixelFormat_Grayscale8; - return true; - - case gdcm::PixelFormat::UINT16: - format = Orthanc::PixelFormat_Grayscale16; - return true; - - case gdcm::PixelFormat::INT16: - format = Orthanc::PixelFormat_SignedGrayscale16; - return true; - - default: - return false; - } - } - else if (image.GetPixelFormat().GetSamplesPerPixel() == 3 && - image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8) - { - format = Orthanc::PixelFormat_RGB24; - return true; - } - else if (image.GetPixelFormat().GetSamplesPerPixel() == 4 && - image.GetPixelFormat().GetScalarType() == gdcm::PixelFormat::UINT8) - { - format = Orthanc::PixelFormat_RGBA32; - return true; - } - - return false; -} - - -ORTHANC_PLUGINS_API OrthancPluginErrorCode DecodeImage(OrthancPluginRestOutput* output, - const char* url, - const OrthancPluginHttpRequest* request) -{ - std::string instance(request->groups[0]); - std::string outputFormat(request->groups[1]); - OrthancContext::GetInstance().LogWarning("Using GDCM to decode instance " + instance); - - // Download the request DICOM instance from Orthanc into a memory buffer - std::string dicom; - OrthancContext::GetInstance().GetDicomForInstance(dicom, instance); - - // Prepare a memory stream over the DICOM instance - std::stringstream stream(dicom); - - // Parse the DICOM instance using GDCM - gdcm::ImageReader imageReader; - imageReader.SetStream(stream); - if (!imageReader.Read()) - { - OrthancContext::GetInstance().LogError("GDCM cannot extract an image from this DICOM instance"); - AnswerUnsupportedImage(output); - return OrthancPluginErrorCode_Success; - } - - gdcm::Image& image = imageReader.GetImage(); - - - // Log information about the decoded image - char tmp[1024]; - sprintf(tmp, "Image format: %dx%d %s with %d color channel(s)", image.GetRows(), image.GetColumns(), - image.GetPixelFormat().GetScalarTypeAsString(), image.GetPixelFormat().GetSamplesPerPixel()); - OrthancContext::GetInstance().LogWarning(tmp); - - - // Convert planar configuration - gdcm::ImageChangePlanarConfiguration planar; - if (image.GetPlanarConfiguration() != 0 && - image.GetPixelFormat().GetSamplesPerPixel() != 1) - { - OrthancContext::GetInstance().LogWarning("Converting planar configuration to interleaved"); - planar.SetInput(imageReader.GetImage()); - planar.Change(); - image = planar.GetOutput(); - } - - - // Create a read-only accessor to the bitmap decoded by GDCM - Orthanc::PixelFormat format; - if (!GetOrthancPixelFormat(format, image)) - { - OrthancContext::GetInstance().LogError("This sample plugin does not support this image format"); - AnswerUnsupportedImage(output); - return OrthancPluginErrorCode_Success; - } - - Orthanc::ImageAccessor decodedImage; - std::vector<char> decodedBuffer(image.GetBufferLength()); - - if (decodedBuffer.size()) - { - image.GetBuffer(&decodedBuffer[0]); - unsigned int pitch = image.GetColumns() * ::Orthanc::GetBytesPerPixel(format); - decodedImage.AssignWritable(format, image.GetColumns(), image.GetRows(), pitch, &decodedBuffer[0]); - } - else - { - // Empty image - decodedImage.AssignWritable(format, 0, 0, 0, NULL); - } - - - // Convert the pixel format from GDCM to the format requested by the REST query - Orthanc::ImageBuffer converted; - converted.SetWidth(decodedImage.GetWidth()); - converted.SetHeight(decodedImage.GetHeight()); - - if (outputFormat == "preview") - { - if (format == Orthanc::PixelFormat_RGB24 || - format == Orthanc::PixelFormat_RGBA32) - { - // Do not rescale color image - converted.SetFormat(Orthanc::PixelFormat_RGB24); - } - else - { - converted.SetFormat(Orthanc::PixelFormat_Grayscale8); - - // Rescale the image to the [0,255] range - int64_t a, b; - Orthanc::ImageProcessing::GetMinMaxValue(a, b, decodedImage); - - float offset = -a; - float scaling = 255.0f / static_cast<float>(b - a); - Orthanc::ImageProcessing::ShiftScale(decodedImage, offset, scaling); - } - } - else - { - if (format == Orthanc::PixelFormat_RGB24 || - format == Orthanc::PixelFormat_RGBA32) - { - // Do not convert color images to grayscale values (this is Orthanc convention) - AnswerUnsupportedImage(output); - return OrthancPluginErrorCode_Success; - } - - if (outputFormat == "image-uint8") - { - converted.SetFormat(Orthanc::PixelFormat_Grayscale8); - } - else if (outputFormat == "image-uint16") - { - converted.SetFormat(Orthanc::PixelFormat_Grayscale16); - } - else if (outputFormat == "image-int16") - { - converted.SetFormat(Orthanc::PixelFormat_SignedGrayscale16); - } - else - { - OrthancContext::GetInstance().LogError("Unknown output format: " + outputFormat); - AnswerUnsupportedImage(output); - return OrthancPluginErrorCode_Success; - } - } - - Orthanc::ImageAccessor convertedAccessor(converted.GetAccessor()); - Orthanc::ImageProcessing::Convert(convertedAccessor, decodedImage); - - // Compress the converted image as a PNG file - OrthancContext::GetInstance().CompressAndAnswerPngImage(output, convertedAccessor); - - return OrthancPluginErrorCode_Success; // Success -} - - -extern "C" -{ - ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) - { - OrthancContext::GetInstance().Initialize(context); - OrthancContext::GetInstance().LogWarning("Initializing GDCM decoding"); - - // Check the version of the Orthanc core - if (OrthancPluginCheckVersion(context) == 0) - { - OrthancContext::GetInstance().LogError( - "Your version of Orthanc (" + std::string(context->orthancVersion) + - ") must be above " + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER) + - "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER) + - "." + boost::lexical_cast<std::string>(ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER) + - " to run this plugin"); - return -1; - } - - OrthancContext::GetInstance().Register("/instances/([^/]+)/(preview|image-uint8|image-uint16|image-int16)", DecodeImage); - return 0; - } - - ORTHANC_PLUGINS_API void OrthancPluginFinalize() - { - OrthancContext::GetInstance().LogWarning("Finalizing GDCM decoding"); - OrthancContext::GetInstance().Finalize(); - } - - ORTHANC_PLUGINS_API const char* OrthancPluginGetName() - { - return "gdcm-decoding"; - } - - ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() - { - return "1.0"; - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ModalityWorklists/CMakeLists.txt Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 2.8) + +project(ModalityWorklists) + +SET(MODALITY_WORKLISTS_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_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") +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/JsonCppConfiguration.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake) + +add_library(ModalityWorklists SHARED + Plugin.cpp + ${JSONCPP_SOURCES} + ${BOOST_SOURCES} + ) + +message("Setting the version of the plugin to ${MODALITY_WORKLISTS_VERSION}") +add_definitions( + -DMODALITY_WORKLISTS_VERSION="${MODALITY_WORKLISTS_VERSION}" + ) + +set_target_properties(ModalityWorklists PROPERTIES + VERSION ${MODALITY_WORKLISTS_VERSION} + SOVERSION ${MODALITY_WORKLISTS_VERSION}) + +install( + TARGETS ModalityWorklists + RUNTIME DESTINATION lib # Destination for Windows + LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,267 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, 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 <http://www.gnu.org/licenses/>. + **/ + + +#include <orthanc/OrthancCPlugin.h> + +#include <boost/filesystem.hpp> +#include <json/value.h> +#include <json/reader.h> +#include <string.h> +#include <iostream> +#include <algorithm> + +static OrthancPluginContext* context_ = NULL; +static std::string folder_; + + +static bool ReadFile(std::string& result, + const std::string& path) +{ + OrthancPluginMemoryBuffer tmp; + if (OrthancPluginReadFile(context_, &tmp, path.c_str())) + { + return false; + } + else + { + result.assign(reinterpret_cast<const char*>(tmp.data), tmp.size); + OrthancPluginFreeMemoryBuffer(context_, &tmp); + return true; + } +} + + +/** + * This is the main function for matching a DICOM worklist against a query. + **/ +static OrthancPluginErrorCode MatchWorklist(OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const std::string& path) +{ + std::string dicom; + if (!ReadFile(dicom, path)) + { + // Cannot read this file, ignore this error + return OrthancPluginErrorCode_Success; + } + + if (OrthancPluginWorklistIsMatch(context_, query, dicom.c_str(), dicom.size())) + { + // This DICOM file matches the worklist query, add it to the answers + return OrthancPluginWorklistAddAnswer + (context_, answers, query, dicom.c_str(), dicom.size()); + } + else + { + // This DICOM file does not match + return OrthancPluginErrorCode_Success; + } +} + + + +static bool ConvertToJson(Json::Value& result, + char* content) +{ + if (content == NULL) + { + return false; + } + else + { + Json::Reader reader; + bool success = reader.parse(content, content + strlen(content), result); + OrthancPluginFreeString(context_, content); + return success; + } +} + + +static bool GetQueryDicom(Json::Value& value, + const OrthancPluginWorklistQuery* query) +{ + OrthancPluginMemoryBuffer dicom; + if (OrthancPluginWorklistGetDicomQuery(context_, &dicom, query)) + { + return false; + } + + char* json = OrthancPluginDicomBufferToJson(context_, reinterpret_cast<const char*>(dicom.data), + dicom.size, + OrthancPluginDicomToJsonFormat_Short, + static_cast<OrthancPluginDicomToJsonFlags>(0), 0); + OrthancPluginFreeMemoryBuffer(context_, &dicom); + + return ConvertToJson(value, json); +} + + +static void ToLowerCase(std::string& s) +{ + std::transform(s.begin(), s.end(), s.begin(), tolower); +} + + +OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* remoteAet, + const char* calledAet) +{ + Json::Value json; + + if (!GetQueryDicom(json, query)) + { + return OrthancPluginErrorCode_InternalError; + } + + std::cout << "Received worklist query from remote modality " << remoteAet + << ":" << std::endl << json.toStyledString(); + + boost::filesystem::path source(folder_); + boost::filesystem::directory_iterator end; + + try + { + for (boost::filesystem::directory_iterator it(source); it != end; ++it) + { + if (is_regular_file(it->status())) + { + std::string extension = boost::filesystem::extension(it->path()); + ToLowerCase(extension); + + if (extension == ".wl") + { + OrthancPluginErrorCode error = MatchWorklist(answers, query, it->path().string()); + if (error) + { + OrthancPluginLogError(context_, "Error while adding an answer to a worklist request"); + return error; + } + } + } + } + } + catch (boost::filesystem::filesystem_error&) + { + std::string description = std::string("Inexistent folder while scanning for worklists: ") + source.string(); + OrthancPluginLogError(context_, description.c_str()); + return OrthancPluginErrorCode_DirectoryExpected; + } + + // Uncomment the following line if too many answers are to be returned + // OrthancPluginMarkWorklistAnswersIncomplete(context_, answers); + + return OrthancPluginErrorCode_Success; +} + + +extern "C" +{ + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) + { + context_ = c; + OrthancPluginLogWarning(context_, "Sample worklist plugin is initializing"); + OrthancPluginSetDescription(context_, "Serve DICOM modality worklists from a folder with Orthanc."); + + /* Check the version of the Orthanc core */ + if (OrthancPluginCheckVersion(c) == 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; + } + + Json::Value configuration; + if (!ConvertToJson(configuration, OrthancPluginGetConfiguration(context_))) + { + OrthancPluginLogError(context_, "Cannot access the configuration of the worklist server"); + return -1; + } + + bool enabled = false; + + if (configuration.isMember("Worklists")) + { + const Json::Value& config = configuration["Worklists"]; + if (!config.isMember("Enable") || + config["Enable"].type() != Json::booleanValue) + { + OrthancPluginLogError(context_, "The configuration option \"Worklists.Enable\" must contain a Boolean"); + return -1; + } + else + { + enabled = config["Enable"].asBool(); + if (enabled) + { + if (!config.isMember("Database") || + config["Database"].type() != Json::stringValue) + { + OrthancPluginLogError(context_, "The configuration option \"Worklists.Database\" must contain a path"); + return -1; + } + + folder_ = config["Database"].asString(); + } + else + { + OrthancPluginLogWarning(context_, "Worklists server is disabled by the configuration file"); + } + } + } + else + { + OrthancPluginLogWarning(context_, "Worklists server is disabled, no suitable configuration section was provided"); + } + + if (enabled) + { + std::string message = "The database of worklists will be read from folder: " + folder_; + OrthancPluginLogWarning(context_, message.c_str()); + + OrthancPluginRegisterWorklistCallback(context_, Callback); + } + + return 0; + } + + + ORTHANC_PLUGINS_API void OrthancPluginFinalize() + { + OrthancPluginLogWarning(context_, "Sample worklist plugin is finalizing"); + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetName() + { + return "worklists"; + } + + + ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion() + { + return MODALITY_WORKLISTS_VERSION; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ModalityWorklists/README Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,60 @@ +Introduction +============ + +This sample plugin enables Orthanc to serve DICOM modality worklists +that are read from some folder. + +Whenever a C-Find SCP request is issued to Orthanc, it will read the +content of this folder to locate the worklists that match the request. +An external application can dynamically modify the content of this +folder while Orthanc is running to add/remove worklists. + +This sample mimics the behavior of the "wlmscpfs" tool from the +DCMTK package: +http://support.dcmtk.org/docs/wlmscpfs.html + + +Compilation for Linux +===================== + +# mkdir Build +# cd Build +# cmake .. +# make + + +Cross-compilation for Windows using MinGW +========================================= + +# mkdir Build +# cd Build +# cmake .. -DCMAKE_TOOLCHAIN_FILE=../../../Resources/MinGWToolchain.cmake +# make + + +Configuration +============= + +First, generate the default configuration of Orthanc: +https://orthanc.chu.ulg.ac.be/book/users/configuration.html + +Then, modify the "Plugins" option to point to the folder containing +the built plugins. Finally, create a section "ModalityWorklists" in +the configuration file to configure the worklist server. If using the +build commands above, a sample configuration would read as follows: + +{ + [...] + "Plugins" : [ + "." + ], + "Worklists" : { + "Enable": true, + "Database": "../WorklistsDatabase" + } +} + +The folder "WorklistsDatabase" contains a database of sample +worklists, that come from the DCMTK source distribution, as described +in the FAQ entry #37 of the DCMTK project: +http://forum.dcmtk.org/viewtopic.php?t=84
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py Wed Dec 02 09:52:56 2015 +0100 @@ -0,0 +1,19 @@ +#!/usr/bin/python + +import os +import subprocess + +SOURCE = '/home/jodogne/Downloads/dcmtk-3.6.0/dcmwlm/data/wlistdb/OFFIS/' +TARGET = os.path.abspath(os.path.dirname(__file__)) + +for f in os.listdir(SOURCE): + ext = os.path.splitext(f) + + if ext[1].lower() == '.dump': + subprocess.check_call([ + 'dump2dcm', + '-g', + '-q', + os.path.join(SOURCE, f), + os.path.join(TARGET, ext[0].lower() + '.wl'), + ])
--- a/Plugins/Samples/ServeFolders/README Wed Nov 18 10:16:21 2015 +0100 +++ b/Plugins/Samples/ServeFolders/README Wed Dec 02 09:52:56 2015 +0100 @@ -27,7 +27,7 @@ ============= First, generate the default configuration of Orthanc: -https://code.google.com/p/orthanc/wiki/OrthancConfiguration +https://orthanc.chu.ulg.ac.be/book/users/configuration.html Then, modify the "Plugins" option to point to the folder containing the built plugins.
--- a/Resources/CMake/BoostConfiguration.cmake Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/CMake/BoostConfiguration.cmake Wed Dec 02 09:52:56 2015 +0100 @@ -39,10 +39,10 @@ if (BOOST_STATIC) - # Parameters for Boost 1.58.0 - set(BOOST_NAME boost_1_58_0) - set(BOOST_BCP_SUFFIX bcpdigest-0.9.2) - set(BOOST_MD5 "704b110917cbda903e07cb53934b47ac") + # Parameters for Boost 1.59.0 + set(BOOST_NAME boost_1_59_0) + set(BOOST_BCP_SUFFIX bcpdigest-0.9.5) + set(BOOST_MD5 "08abb7cdbea0b380f9ab0d5cce476f12") set(BOOST_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz") set(BOOST_FILESYSTEM_SOURCES_DIR "${BOOST_NAME}/libs/filesystem/src") set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
--- a/Resources/CMake/BoostConfiguration.sh Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/CMake/BoostConfiguration.sh Wed Dec 02 09:52:56 2015 +0100 @@ -13,22 +13,23 @@ ## History: ## - Orthanc between 0.6.2 and 0.7.3: Boost 1.54.0 ## - Orthanc between 0.7.4 and 0.9.1: Boost 1.55.0 -## - Orthanc >= 0.9.2: Boost 1.58.0 +## - Orthanc between 0.9.2 and 0.9.4: Boost 1.58.0 +## - Orthanc >= 0.9.5: Boost 1.59.0 -rm -rf /tmp/boost_1_58_0 -rm -rf /tmp/bcp/boost_1_58_0 +rm -rf /tmp/boost_1_59_0 +rm -rf /tmp/bcp/boost_1_59_0 cd /tmp -echo "Uncompressing the sources of Boost 1.58.0..." -tar xfz ./boost_1_58_0.tar.gz +echo "Uncompressing the sources of Boost 1.59.0..." +tar xfz ./boost_1_59_0.tar.gz echo "Generating the subset..." -mkdir -p /tmp/bcp/boost_1_58_0 -bcp --boost=/tmp/boost_1_58_0 thread system locale date_time filesystem math/special_functions algorithm uuid atomic /tmp/bcp/boost_1_58_0 +mkdir -p /tmp/bcp/boost_1_59_0 +bcp --boost=/tmp/boost_1_59_0 thread system locale date_time filesystem math/special_functions algorithm uuid atomic iostreams /tmp/bcp/boost_1_59_0 cd /tmp/bcp echo "Compressing the subset..." -tar cfz boost_1_58_0_bcpdigest-0.9.2.tar.gz boost_1_58_0 -ls -l boost_1_58_0_bcpdigest-0.9.2.tar.gz -md5sum boost_1_58_0_bcpdigest-0.9.2.tar.gz -readlink -f boost_1_58_0_bcpdigest-0.9.2.tar.gz +tar cfz boost_1_59_0_bcpdigest-0.9.5.tar.gz boost_1_59_0 +ls -l boost_1_59_0_bcpdigest-0.9.5.tar.gz +md5sum boost_1_59_0_bcpdigest-0.9.5.tar.gz +readlink -f boost_1_59_0_bcpdigest-0.9.5.tar.gz
--- a/Resources/CMake/DcmtkConfiguration.cmake Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/CMake/DcmtkConfiguration.cmake Wed Dec 02 09:52:56 2015 +0100 @@ -1,6 +1,6 @@ if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK) SET(DCMTK_VERSION_NUMBER 361) - set(DCMTK_PACKAGE_VERSION "3.6.1") + SET(DCMTK_PACKAGE_VERSION "3.6.1") SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.1_20150629) SET(DCMTK_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/dcmtk-3.6.1_20150629.tar.gz") SET(DCMTK_MD5 "2faf73786fc638ae05fef0103cce0eea")
--- a/Resources/Configuration.json Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/Configuration.json Wed Dec 02 09:52:56 2015 +0100 @@ -102,6 +102,10 @@ "Mpeg2TransferSyntaxAccepted" : true, "RleTransferSyntaxAccepted" : true, + // Whether Orthanc accepts to act as C-Store SCP for unknown storage + // SOP classes (aka. "promiscuous mode") + "UnknownSopClassAccepted" : true, + /** @@ -146,8 +150,9 @@ * A fourth parameter is available to enable patches for a * specific PACS manufacturer. The allowed values are currently * "Generic" (default value), "StoreScp" (storescp tool from - * DCMTK), "ClearCanvas", "MedInria", "Dcm4Chee" and - * "SyngoVia". This parameter is case-sensitive. + * DCMTK), "ClearCanvas", "MedInria", "Dcm4Chee", "SyngoVia", + * "AgfaImpax" (Agfa IMPAX), "EFilm2" (eFilm version 2), and + * "Vitrea". This parameter is case-sensitive. **/ // "clearcanvas" : [ "CLEARCANVAS", "192.168.1.1", 104, "ClearCanvas" ] },
--- a/Resources/DicomConformanceStatement.txt Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/DicomConformanceStatement.txt Wed Dec 02 09:52:56 2015 +0100 @@ -149,6 +149,7 @@ FINDPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.1 FINDStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.1 + FINDModalityWorklistInformationModel | 1.2.840.10008.5.1.4.31 --------------------
--- a/Resources/ErrorCodes.json Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/ErrorCodes.json Wed Dec 02 09:52:56 2015 +0100 @@ -513,5 +513,10 @@ "Code": 2040, "Name": "CannotOrderSlices", "Description": "Unable to order the slices of the series" + }, + { + "Code": 2041, + "Name": "NoWorklistHandler", + "Description": "No request handler factory for DICOM C-Find Modality SCP" } ]
--- a/Resources/Samples/Lua/TransferSyntaxDisable.lua Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/Samples/Lua/TransferSyntaxDisable.lua Wed Dec 02 09:52:56 2015 +0100 @@ -26,4 +26,8 @@ return false end +function IsUnknownSopClassAccepted(aet, ip) + return false +end + print('All special transfer syntaxes are now disallowed')
--- a/Resources/Samples/Lua/TransferSyntaxEnable.lua Wed Nov 18 10:16:21 2015 +0100 +++ b/Resources/Samples/Lua/TransferSyntaxEnable.lua Wed Dec 02 09:52:56 2015 +0100 @@ -26,4 +26,8 @@ return true end +function IsUnknownSopClassAccepted(aet, ip) + return true +end + print('All special transfer syntaxes are now accepted')
--- a/UnitTestsSources/DicomMapTests.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/UnitTestsSources/DicomMapTests.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -151,6 +151,9 @@ static void TestModule(ResourceType level, DicomModule module) { + // REFERENCE: DICOM PS3.3 2015c - Information Object Definitions + // http://dicom.nema.org/medical/dicom/current/output/html/part03.html + std::set<DicomTag> moduleTags, main; DicomTag::AddTagsForModule(moduleTags, module); DicomMap::GetMainDicomTags(main, level); @@ -160,21 +163,41 @@ { bool ok = moduleTags.find(*it) != moduleTags.end(); - // Exceptions for the Series level - /*if ((// - *it == DicomTag(0x, 0x) && - level == ResourceType_Series)) + // Exceptions for the Study level + if (level == ResourceType_Study && + (*it == DicomTag(0x0008, 0x0080) || /* InstitutionName, from Visit identification module, related to Visit */ + *it == DicomTag(0x0032, 0x1032) || /* RequestingPhysician, from Imaging Service Request module, related to Study */ + *it == DicomTag(0x0032, 0x1060))) /* RequestedProcedureDescription, from Requested Procedure module, related to Study */ { ok = true; - }*/ + } + + // Exceptions for the Series level + if (level == ResourceType_Series && + (*it == DicomTag(0x0008, 0x0070) || /* Manufacturer, from General Equipment Module */ + *it == DicomTag(0x0008, 0x1010) || /* StationName, from General Equipment Module */ + *it == DicomTag(0x0018, 0x0024) || /* SequenceName, from MR Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0018, 0x1090) || /* CardiacNumberOfImages, from MR Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0020, 0x0037) || /* ImageOrientationPatient, from Image Plane Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0020, 0x0105) || /* NumberOfTemporalPositions, from MR Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0020, 0x1002) || /* ImagesInAcquisition, from General Image Module (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0054, 0x0081) || /* NumberOfSlices, from PET Series module */ + *it == DicomTag(0x0054, 0x0101) || /* NumberOfTimeSlices, from PET Series module */ + *it == DicomTag(0x0054, 0x1000) || /* SeriesType, from PET Series module */ + *it == DicomTag(0x0018, 0x1400) || /* AcquisitionDeviceProcessingDescription, from CR/X-Ray/DX/WholeSlideMicro Image (SIMPLIFICATION => Series) */ + *it == DicomTag(0x0018, 0x0010))) /* ContrastBolusAgent, from Contrast/Bolus module (SIMPLIFICATION => Series) */ + { + ok = true; + } // Exceptions for the Instance level if (level == ResourceType_Instance && - (*it == DicomTag(0x0020, 0x0012) || /* Accession number, from Image module */ - *it == DicomTag(0x0054, 0x1330) || /* Image Index, from PET Image module */ - *it == DicomTag(0x0020, 0x0100) || /* Temporal Position Identifier, from MR Image module */ - *it == DicomTag(0x0028, 0x0008) || /* Number of Frames, from Multi-frame module attributes, related to Image IOD */ - *it == DICOM_TAG_IMAGE_POSITION_PATIENT)) + (*it == DicomTag(0x0020, 0x0012) || /* AccessionNumber, from General Image module */ + *it == DicomTag(0x0054, 0x1330) || /* ImageIndex, from PET Image module */ + *it == DicomTag(0x0020, 0x0100) || /* TemporalPositionIdentifier, from MR Image module */ + *it == DicomTag(0x0028, 0x0008) || /* NumberOfFrames, from Multi-frame module attributes, related to Image */ + *it == DicomTag(0x0020, 0x0032) || /* ImagePositionPatient, from Image Plan module, related to Image */ + *it == DicomTag(0x0020, 0x4000))) /* ImageComments, from General Image module */ { ok = true; } @@ -194,6 +217,6 @@ { TestModule(ResourceType_Patient, DicomModule_Patient); TestModule(ResourceType_Study, DicomModule_Study); - //TestModule(ResourceType_Series, DicomModule_Series); // TODO + TestModule(ResourceType_Series, DicomModule_Series); // TODO TestModule(ResourceType_Instance, DicomModule_Instance); }
--- a/UnitTestsSources/FromDcmtkTests.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/UnitTestsSources/FromDcmtkTests.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -43,6 +43,7 @@ #include "../Core/Images/PngWriter.h" #include "../Core/Uuid.h" #include "../Resources/EncodingTests.h" +#include "../OrthancServer/DicomProtocol/DicomFindAnswers.h" #include <dcmtk/dcmdata/dcelem.h> @@ -74,7 +75,7 @@ //m.Replace(DICOM_TAG_PATIENT_ID, "coucou"); //m.Replace(DICOM_TAG_PATIENT_NAME, "coucou"); - ParsedDicomFile o; + ParsedDicomFile o(true); o.SaveToFile("UnitTestsResults/anon.dcm"); for (int i = 0; i < 10; i++) @@ -96,13 +97,13 @@ const DicomTag privateTag(0x0045, 0x0010); const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020")); - ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag)); - ASSERT_TRUE(FromDcmtkBridge::IsPrivateTag(privateTag2)); + ASSERT_TRUE(privateTag.IsPrivate()); + ASSERT_TRUE(privateTag2.IsPrivate()); ASSERT_EQ(0x0031, privateTag2.GetGroup()); ASSERT_EQ(0x1020, privateTag2.GetElement()); std::string s; - ParsedDicomFile o; + ParsedDicomFile o(true); o.Replace(DICOM_TAG_PATIENT_NAME, "coucou"); ASSERT_FALSE(o.GetTagValue(s, privateTag)); o.Insert(privateTag, "private tag", false); @@ -160,7 +161,7 @@ ASSERT_EQ(5u, reader.GetWidth()); ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat()); - ParsedDicomFile o; + ParsedDicomFile o(true); o.EmbedContent(s); o.SaveToFile("UnitTestsResults/png1.dcm"); @@ -266,7 +267,7 @@ std::string dicom; { - ParsedDicomFile f; + ParsedDicomFile f(true); f.SetEncoding(testEncodings[i]); std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i]); @@ -406,7 +407,7 @@ TEST(ParsedDicomFile, InsertReplaceStrings) { - ParsedDicomFile f; + ParsedDicomFile f(true); f.Insert(DICOM_TAG_PATIENT_NAME, "World", false); ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false), OrthancException); // Already existing tag @@ -445,7 +446,7 @@ TEST(ParsedDicomFile, InsertReplaceJson) { - ParsedDicomFile f; + ParsedDicomFile f(true); Json::Value a; CreateSampleJson(a); @@ -497,7 +498,7 @@ TEST(ParsedDicomFile, JsonEncoding) { - ParsedDicomFile f; + ParsedDicomFile f(true); for (unsigned int i = 0; i < testEncodingsCount; i++) { @@ -527,7 +528,7 @@ FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), EVR_PN, "MyPrivateTag", 1, 1); FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), EVR_PN, "Declared public tag", 1, 1); - ParsedDicomFile f; + ParsedDicomFile f(true); f.Insert(DicomTag(0x7050, 0x1000), "Some public tag", false); // Even group => public tag f.Insert(DicomTag(0x7052, 0x1000), "Some unknown tag", false); // Even group => public, unknown tag f.Insert(DicomTag(0x7053, 0x1000), "Some private tag", false); // Odd group => private tag @@ -542,6 +543,15 @@ ASSERT_EQ(Json::stringValue, v["7050,1000"].type()); ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7, v.getMemberNames().size()); + ASSERT_FALSE(v.isMember("7052,1000")); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); + f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePrivateTags, 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(7, v.getMemberNames().size()); @@ -549,32 +559,48 @@ ASSERT_TRUE(v.isMember("7050,1000")); ASSERT_TRUE(v.isMember("7053,1000")); ASSERT_EQ("Some public tag", v["7050,1000"].asString()); - ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); // TODO SHOULD BE STRING + std::string mime, content; + ASSERT_EQ(Json::stringValue, v["7053,1000"].type()); + Toolbox::DecodeDataUriScheme(mime, content, v["7053,1000"].asString()); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ("Some private tag", content); - f.ToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludeUnknownTags, 0); + f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_ConvertBinaryToNull), 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(7, v.getMemberNames().size()); ASSERT_TRUE(v.isMember("7050,1000")); ASSERT_TRUE(v.isMember("7052,1000")); ASSERT_FALSE(v.isMember("7053,1000")); ASSERT_EQ("Some public tag", v["7050,1000"].asString()); - ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); // TODO SHOULD BE STRING + ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); - f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags), 0); + f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags), 0); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ(7, v.getMemberNames().size()); + ASSERT_TRUE(v.isMember("7050,1000")); + ASSERT_TRUE(v.isMember("7052,1000")); + ASSERT_FALSE(v.isMember("7053,1000")); + ASSERT_EQ("Some public tag", v["7050,1000"].asString()); + ASSERT_EQ(Json::stringValue, v["7052,1000"].type()); + Toolbox::DecodeDataUriScheme(mime, content, v["7052,1000"].asString()); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ("Some unknown tag", content); + + f.ToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_ConvertBinaryToNull), 0); ASSERT_EQ(Json::objectValue, v.type()); ASSERT_EQ(8, v.getMemberNames().size()); ASSERT_TRUE(v.isMember("7050,1000")); ASSERT_TRUE(v.isMember("7052,1000")); ASSERT_TRUE(v.isMember("7053,1000")); ASSERT_EQ("Some public tag", v["7050,1000"].asString()); - ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); // TODO SHOULD BE STRING - ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); // TODO SHOULD BE STRING + ASSERT_EQ(Json::nullValue, v["7052,1000"].type()); + ASSERT_EQ(Json::nullValue, v["7053,1000"].type()); } TEST(ParsedDicomFile, ToJsonFlags2) { - ParsedDicomFile f; + ParsedDicomFile f(true); f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false); Json::Value v; @@ -606,3 +632,131 @@ ASSERT_EQ("application/octet-stream", mime); ASSERT_EQ("Pixels", content); } + + +TEST(DicomFindAnswers, Basic) +{ + DicomFindAnswers a; + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_ID, "hello"); + a.Add(m); + } + + { + ParsedDicomFile d(true); + d.Replace(DICOM_TAG_PATIENT_ID, "my"); + a.Add(d); + } + + { + DicomMap m; + m.SetValue(DICOM_TAG_PATIENT_ID, "world"); + a.Add(m); + } + + Json::Value j; + a.ToJson(j, true); + ASSERT_EQ(3u, j.size()); + + //std::cout << j; +} + + +TEST(ParsedDicomFile, FromJson) +{ + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7057, 0x1000), EVR_OB, "MyPrivateTag", 1, 1); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7059, 0x1000), EVR_OB, "MyPrivateTag", 1, 1); + FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), EVR_PN, "Declared public tag", 1, 1); + + Json::Value v; + const std::string sopClassUid = "1.2.840.10008.5.1.4.1.1.1"; // CR Image Storage: + + { + v["SOPClassUID"] = sopClassUid; + v["SpecificCharacterSet"] = "ISO_IR 148"; // This is latin-5 + v["PatientName"] = "Sébastien"; + v["7050-1000"] = "Some public tag"; // Even group => public tag + v["7052-1000"] = "Some unknown tag"; // Even group => public, unknown tag + v["7057-1000"] = "Some private tag"; // Odd group => private tag + v["7059-1000"] = "Some private tag2"; // Odd group => private tag, with an odd length to test padding + + std::string s; + Toolbox::EncodeDataUriScheme(s, "application/octet-stream", "Sebastien"); + v["StudyDescription"] = s; + + v["PixelData"] = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // A red dot of 5x5 pixels + v["0040,0100"] = Json::arrayValue; // ScheduledProcedureStepSequence + + Json::Value vv; + vv["Modality"] = "MR"; + v["0040,0100"].append(vv); + + vv["Modality"] = "CT"; + v["0040,0100"].append(vv); + } + + const DicomToJsonFlags toJsonFlags = static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeBinary | + DicomToJsonFlags_IncludePixelData | + DicomToJsonFlags_IncludePrivateTags | + DicomToJsonFlags_IncludeUnknownTags | + DicomToJsonFlags_ConvertBinaryToAscii); + + + { + std::auto_ptr<ParsedDicomFile> dicom + (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers))); + + Json::Value vv; + dicom->ToJson(vv, DicomToJsonFormat_Simple, toJsonFlags, 0); + + ASSERT_EQ(vv["SOPClassUID"].asString(), sopClassUid); + ASSERT_EQ(vv["MediaStorageSOPClassUID"].asString(), sopClassUid); + ASSERT_TRUE(vv.isMember("SOPInstanceUID")); + ASSERT_TRUE(vv.isMember("SeriesInstanceUID")); + ASSERT_TRUE(vv.isMember("StudyInstanceUID")); + ASSERT_TRUE(vv.isMember("PatientID")); + } + + + { + std::auto_ptr<ParsedDicomFile> dicom + (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_GenerateIdentifiers))); + + Json::Value vv; + dicom->ToJson(vv, DicomToJsonFormat_Simple, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData), 0); + + std::string mime, content; + Toolbox::DecodeDataUriScheme(mime, content, vv["PixelData"].asString()); + ASSERT_EQ("application/octet-stream", mime); + ASSERT_EQ(5u * 5u * 3u /* the red dot is 5x5 pixels in RGB24 */ + 1 /* for padding */, content.size()); + } + + + { + std::auto_ptr<ParsedDicomFile> dicom + (ParsedDicomFile::CreateFromJson(v, static_cast<DicomFromJsonFlags>(DicomFromJsonFlags_DecodeDataUriScheme))); + + Json::Value vv; + dicom->ToJson(vv, DicomToJsonFormat_Short, toJsonFlags, 0); + + ASSERT_FALSE(vv.isMember("SOPInstanceUID")); + ASSERT_FALSE(vv.isMember("SeriesInstanceUID")); + ASSERT_FALSE(vv.isMember("StudyInstanceUID")); + ASSERT_FALSE(vv.isMember("PatientID")); + ASSERT_EQ(2u, vv["0040,0100"].size()); + ASSERT_EQ("MR", vv["0040,0100"][0]["0008,0060"].asString()); + ASSERT_EQ("CT", vv["0040,0100"][1]["0008,0060"].asString()); + ASSERT_EQ("Some public tag", vv["7050,1000"].asString()); + ASSERT_EQ("Some unknown tag", vv["7052,1000"].asString()); + ASSERT_EQ("Some private tag", vv["7057,1000"].asString()); + ASSERT_EQ("Some private tag2", vv["7059,1000"].asString()); + ASSERT_EQ("Sébastien", vv["0010,0010"].asString()); + ASSERT_EQ("Sebastien", vv["0008,1030"].asString()); + ASSERT_EQ("ISO_IR 148", vv["0008,0005"].asString()); + ASSERT_EQ("5", vv[DICOM_TAG_ROWS.Format()].asString()); + ASSERT_EQ("5", vv[DICOM_TAG_COLUMNS.Format()].asString()); + ASSERT_TRUE(vv[DICOM_TAG_PIXEL_DATA.Format()].asString().empty()); + } +}
--- a/UnitTestsSources/ServerIndexTests.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/UnitTestsSources/ServerIndexTests.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -796,9 +796,10 @@ instance.SetValue(DICOM_TAG_SOP_INSTANCE_UID, "instance-" + id); std::map<MetadataType, std::string> instanceMetadata; - ServerIndex::MetadataMap metadata; - ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, instance, attachments, "", metadata)); - ASSERT_EQ(2u, instanceMetadata.size()); + DicomInstanceToStore toStore; + toStore.SetSummary(instance); + ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments)); + ASSERT_EQ(3u, instanceMetadata.size()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_RemoteAet) != instanceMetadata.end()); ASSERT_TRUE(instanceMetadata.find(MetadataType_Instance_ReceptionDate) != instanceMetadata.end());
--- a/UnitTestsSources/UnitTestsMain.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/UnitTestsSources/UnitTestsMain.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -529,6 +529,9 @@ ASSERT_STREQ("MedInria", EnumerationToString(StringToModalityManufacturer("MedInria"))); ASSERT_STREQ("Dcm4Chee", EnumerationToString(StringToModalityManufacturer("Dcm4Chee"))); ASSERT_STREQ("SyngoVia", EnumerationToString(StringToModalityManufacturer("SyngoVia"))); + ASSERT_STREQ("AgfaImpax", EnumerationToString(StringToModalityManufacturer("AgfaImpax"))); + ASSERT_STREQ("EFilm2", EnumerationToString(StringToModalityManufacturer("EFilm2"))); + ASSERT_STREQ("Vitrea", EnumerationToString(StringToModalityManufacturer("Vitrea"))); }
--- a/UnitTestsSources/VersionsTests.cpp Wed Nov 18 10:16:21 2015 +0100 +++ b/UnitTestsSources/VersionsTests.cpp Wed Dec 02 09:52:56 2015 +0100 @@ -102,7 +102,7 @@ TEST(Versions, BoostStatic) { - ASSERT_STREQ("1_58", BOOST_LIB_VERSION); + ASSERT_STREQ("1_59", BOOST_LIB_VERSION); } TEST(Versions, CurlStatic)