Mercurial > hg > orthanc
changeset 5319:f2e1ad71e49c
added "OrthancPluginLoadDicomInstance()" to load DICOM instances from the database
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Sat, 24 Jun 2023 12:18:58 +0200 |
parents | 68e15471b408 |
children | e4c3950345e9 |
files | NEWS OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Samples/Basic/Plugin.c |
diffstat | 8 files changed, 409 insertions(+), 29 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Fri Jun 23 18:01:55 2023 +0200 +++ b/NEWS Sat Jun 24 12:18:58 2023 +0200 @@ -5,8 +5,13 @@ -------- * API version upgraded to 21 -* added a route to delete the output of an asynchronous job (right now only for archive jobs): - e.g. DELETE /jobs/../archive +* Added a route to delete the output of an asynchronous job (right now + only for archive jobs): e.g. DELETE /jobs/../archive + +Plugins +------- + +* Added "OrthancPluginLoadDicomInstance()" to load DICOM instances from the database Maintenance ----------- @@ -15,8 +20,6 @@ tag (0028,0006) equals 1 * Made Orthanc more resilient to common spelling errors in SpecificCharacterSet * Modality worklists plugin: allow searching on private tags (exact match only) -* Upgraded dependencies for static builds: - - boost 1.82.0 * Fix orphan files remaining in storage when working with MaximumStorageSize (https://discourse.orthanc-server.org/t/issue-with-deleting-incoming-dicoms-when-maximumstoragesize-is-reached/3510) * When deleting a resource, its parents LastUpdate metadata are now updated. @@ -25,6 +28,8 @@ (https://discourse.orthanc-server.org/t/orthanc-convert-ybr-to-rgb-but-does-not-change-metadata/3533). This might have an impact on the image returned by /dicom-web/studies/../series/../instances/../frames/1; the image format is now consistent with the PhotometricIntepretation DICOM Tag. +* Upgraded dependencies for static builds: + - boost 1.82.0 Version 1.12.0 (2023-04-14)
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Fri Jun 23 18:01:55 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp Sat Jun 24 12:18:58 2023 +0200 @@ -2124,27 +2124,24 @@ void ParsedDicomFile::InjectEmptyPixelData(ValueRepresentation vr) { - DcmTag k(DICOM_TAG_PIXEL_DATA.GetGroup(), - DICOM_TAG_PIXEL_DATA.GetElement()); - - DcmItem& dataset = *GetDcmtkObjectConst().getDataset(); + DcmItem& dataset = *GetDcmtkObject().getDataset(); DcmElement *element = NULL; - if (!dataset.findAndGetElement(k, element).good() || + if (!dataset.findAndGetElement(DCM_PixelData, element).good() || element == NULL) { // The pixel data is indeed nonexistent, insert it now switch (vr) { case ValueRepresentation_OtherByte: - if (!dataset.putAndInsertUint8Array(k, NULL, 0).good()) + if (!dataset.putAndInsertUint8Array(DCM_PixelData, NULL, 0).good()) { throw OrthancException(ErrorCode_InternalError); } break; case ValueRepresentation_OtherWord: - if (!dataset.putAndInsertUint16Array(k, NULL, 0).good()) + if (!dataset.putAndInsertUint16Array(DCM_PixelData, NULL, 0).good()) { throw OrthancException(ErrorCode_InternalError); } @@ -2157,6 +2154,81 @@ } + void ParsedDicomFile::RemoveFromPixelData() + { + DcmItem& dataset = *GetDcmtkObject().getDataset(); + + // We need to go backward, otherwise "dataset.card()" is invalidated + for (unsigned long i = dataset.card(); i > 0; i--) + { + DcmElement* element = dataset.getElement(i - 1); + if (element == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (element->getTag().getGroup() > DCM_PixelData.getGroup() || + (element->getTag().getGroup() == DCM_PixelData.getGroup() && + element->getTag().getElement() >= DCM_PixelData.getElement())) + { + std::unique_ptr<DcmElement> removal(dataset.remove(i - 1)); + } + } + } + + + ValueRepresentation ParsedDicomFile::GuessPixelDataValueRepresentation() const + { + /** + * DICOM specification is at: + * https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_d.html + * + * Our algorithm for guessing the pixel data VR is imperfect, and + * inspired from: https://forum.dcmtk.org/viewtopic.php?t=4961 + * + * "The baseline for Little Endian Implicit/Explicit is: (a) if + * the TS is Explicit Little Endian and the pixeldata is <= 8bpp, + * VR of pixel data shall be VR_OB, and (b) in all other cases, VR + * of pixel data shall be VR_OW." + **/ + + DicomTransferSyntax ts; + if (LookupTransferSyntax(ts)) + { + if (ts == DicomTransferSyntax_LittleEndianExplicit || + ts == DicomTransferSyntax_BigEndianExplicit) + { + DcmItem& dataset = *GetDcmtkObjectConst().getDataset(); + + uint16_t bitsAllocated; + if (dataset.findAndGetUint16(DCM_BitsAllocated, bitsAllocated).good() && + bitsAllocated > 8) + { + return ValueRepresentation_OtherWord; + } + else + { + return ValueRepresentation_OtherByte; + } + } + else if (ts == DicomTransferSyntax_LittleEndianImplicit) + { + return ValueRepresentation_OtherWord; + } + else + { + // Assume "OB" for all the compressed transfer syntaxes + return ValueRepresentation_OtherByte; + } + } + else + { + // Assume "OB" if transfer syntax is not available + return ValueRepresentation_OtherByte; + } + } + + #if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1 // Alias for binary compatibility with Orthanc Framework 1.7.2 => don't use it anymore void ParsedDicomFile::DatasetToJson(Json::Value& target,
--- a/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Fri Jun 23 18:01:55 2023 +0200 +++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h Sat Jun 24 12:18:58 2023 +0200 @@ -311,5 +311,10 @@ int& originY) const; void InjectEmptyPixelData(ValueRepresentation vr); + + // Remove all the tags after pixel data + void RemoveFromPixelData(); + + ValueRepresentation GuessPixelDataValueRepresentation() const; }; }
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Fri Jun 23 18:01:55 2023 +0200 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Sat Jun 24 12:18:58 2023 +0200 @@ -3264,6 +3264,54 @@ } +#include "../Sources/DicomFormat/DicomArray.h" +TEST(ParsedDicomFile, RemoveFromPixelData) +{ + ParsedDicomFile dicom(true); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0000), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0009), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertUint8Array(DcmTag(0x7fe0, 0x0010), NULL, 0).good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe0, 0x0011), "").good()); + ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString(DcmTag(0x7fe1, 0x0000), "").good()); + + { + DicomMap m; + dicom.ExtractDicomSummary(m, 0); + + ASSERT_EQ(10u, m.GetSize()); + ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PIXEL_DATA)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0011)); + ASSERT_TRUE(m.HasTag(0x7fe1, 0x0000)); + } + + dicom.RemoveFromPixelData(); + + { + DicomMap m; + dicom.ExtractDicomSummary(m, 0); + + ASSERT_EQ(7u, m.GetSize()); + ASSERT_TRUE(m.HasTag(DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0000)); + ASSERT_TRUE(m.HasTag(0x7fe0, 0x0009)); + ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA)); + ASSERT_FALSE(m.HasTag(0x7fe0, 0x0011)); + ASSERT_FALSE(m.HasTag(0x7fe1, 0x0000)); + } +} + + TEST(ParsedDicomFile, DISABLED_InjectEmptyPixelData2) { static const char* PIXEL_DATA = "7FE00010";
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Fri Jun 23 18:01:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Sat Jun 24 12:18:58 2023 +0200 @@ -2060,6 +2060,7 @@ sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction) || + sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode) || 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) || @@ -2502,9 +2503,8 @@ std::string buffer_; std::unique_ptr<DicomInstanceToStore> instance_; - public: - DicomInstanceFromBuffer(const void* buffer, - size_t size) + void Setup(const void* buffer, + size_t size) { buffer_.assign(reinterpret_cast<const char*>(buffer), size); @@ -2512,6 +2512,18 @@ instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); } + public: + DicomInstanceFromBuffer(const void* buffer, + size_t size) + { + Setup(buffer, size); + } + + DicomInstanceFromBuffer(const std::string& buffer) + { + Setup(buffer.empty() ? NULL : buffer.c_str(), buffer.size()); + } + virtual bool CanBeFreed() const ORTHANC_OVERRIDE { return true; @@ -2524,23 +2536,36 @@ }; - class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance + class OrthancPlugins::DicomInstanceFromParsed : public IDicomInstance { private: std::unique_ptr<ParsedDicomFile> parsed_; std::unique_ptr<DicomInstanceToStore> instance_; - public: - explicit DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) : - parsed_(transcoded.ReleaseAsParsedDicomFile()) - { + void Setup(ParsedDicomFile* parsed) + { + parsed_.reset(parsed); + if (parsed_.get() == NULL) { - throw OrthancException(ErrorCode_InternalError); - } - - instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); - instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + throw OrthancException(ErrorCode_NullPointer); + } + else + { + instance_.reset(DicomInstanceToStore::CreateFromParsedDicomFile(*parsed_)); + instance_->SetOrigin(DicomInstanceOrigin::FromPlugins()); + } + } + + public: + explicit DicomInstanceFromParsed(IDicomTranscoder::DicomImage& transcoded) + { + Setup(transcoded.ReleaseAsParsedDicomFile()); + } + + explicit DicomInstanceFromParsed(ParsedDicomFile* parsed /* takes ownership */) + { + Setup(parsed); } virtual bool CanBeFreed() const ORTHANC_OVERRIDE @@ -4386,7 +4411,99 @@ reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendMultipartItem(p.answer, p.answerSize, headers); } - + + + void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params) + { + std::unique_ptr<IDicomInstance> target; + + switch (params.mode) + { + case OrthancPluginLoadDicomInstanceMode_WholeDicom: + { + std::string buffer; + + { + PImpl::ServerContextLock lock(*pimpl_); + lock.GetContext().ReadDicom(buffer, params.instanceId); + } + + target.reset(new DicomInstanceFromBuffer(buffer)); + break; + } + + case OrthancPluginLoadDicomInstanceMode_UntilPixelData: + case OrthancPluginLoadDicomInstanceMode_EmptyPixelData: + { + std::unique_ptr<ParsedDicomFile> parsed; + + { + std::string buffer; + + { + PImpl::ServerContextLock lock(*pimpl_); + if (!lock.GetContext().ReadDicomUntilPixelData(buffer, params.instanceId)) + { + lock.GetContext().ReadDicom(buffer, params.instanceId); + } + } + + parsed.reset(new ParsedDicomFile(buffer)); + } + + parsed->RemoveFromPixelData(); + + if (params.mode == OrthancPluginLoadDicomInstanceMode_EmptyPixelData) + { + ValueRepresentation vr = parsed->GuessPixelDataValueRepresentation(); + + // Try and retrieve the VR of pixel data from the metadata of the instance + { + PImpl::ServerContextLock lock(*pimpl_); + + std::string s; + int64_t revision; // unused + if (lock.GetContext().GetIndex().LookupMetadata( + s, revision, params.instanceId, + ResourceType_Instance, MetadataType_Instance_PixelDataVR)) + { + if (s == "OB") + { + vr = ValueRepresentation_OtherByte; + } + else if (s == "OW") + { + vr = ValueRepresentation_OtherWord; + } + else + { + LOG(WARNING) << "Corrupted PixelDataVR metadata associated with instance " + << params.instanceId << ": " << s; + } + } + } + + parsed->InjectEmptyPixelData(vr); + } + + target.reset(new DicomInstanceFromParsed(parsed.release())); + break; + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (target.get() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + *params.target = reinterpret_cast<OrthancPluginDicomInstance*>(target.release()); + } + } + void OrthancPlugins::DatabaseAnswer(const void* parameters) { @@ -5279,7 +5396,7 @@ if (success) { *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>( - new DicomInstanceFromTranscoded(transcoded)); + new DicomInstanceFromParsed(transcoded)); return true; } else @@ -5341,6 +5458,14 @@ RegisterIncomingHttpRequestFilter2(parameters); return true; + case _OrthancPluginService_LoadDicomInstance: + { + const _OrthancPluginLoadDicomInstance& p = + *reinterpret_cast<const _OrthancPluginLoadDicomInstance*>(parameters); + ApplyLoadDicomInstance(p); + return true; + } + default: return false; }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Fri Jun 23 18:01:55 2023 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Sat Jun 24 12:18:58 2023 +0200 @@ -89,7 +89,7 @@ class IDicomInstance; class DicomInstanceFromCallback; class DicomInstanceFromBuffer; - class DicomInstanceFromTranscoded; + class DicomInstanceFromParsed; class WebDavCollection; void RegisterRestCallback(const void* parameters, @@ -217,6 +217,8 @@ void ApplySendMultipartItem2(const void* parameters); + void ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& parameters); + void ComputeHash(_OrthancPluginService service, const void* parameters);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Jun 23 18:01:55 2023 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Sat Jun 24 12:18:58 2023 +0200 @@ -120,7 +120,7 @@ #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER 1 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER 12 -#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 0 +#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER 1 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE) @@ -526,6 +526,7 @@ _OrthancPluginService_GetInstanceAdvancedJson = 4017, /* New in Orthanc 1.7.0 */ _OrthancPluginService_GetInstanceDicomWebJson = 4018, /* New in Orthanc 1.7.0 */ _OrthancPluginService_GetInstanceDicomWebXml = 4019, /* New in Orthanc 1.7.0 */ + _OrthancPluginService_LoadDicomInstance = 4020, /* New in Orthanc 1.12.1 */ /* Services for plugins implementing a database back-end */ _OrthancPluginService_RegisterDatabaseBackend = 5000, /* New in Orthanc 0.8.6 */ @@ -1024,6 +1025,28 @@ /** + * Mode specifying how to load a DICOM instance. + * @see OrthancPluginLoadDicomInstance + **/ + typedef enum + { + OrthancPluginLoadDicomInstanceMode_WholeDicom = 1, + /*!< Load the whole DICOM file, including pixel data */ + + OrthancPluginLoadDicomInstanceMode_UntilPixelData = 2, + /*!< Load the whole DICOM file until pixel data, which will speed + up the loading */ + + OrthancPluginLoadDicomInstanceMode_EmptyPixelData = 3, + /*!< Load the whole DICOM file until pixel data, and replace pixel + data by an empty tag whose VR (value representation) is the same + as those of the original DICOM file */ + + _OrthancPluginLoadDicomInstanceMode_INTERNAL = 0x7fffffff + } OrthancPluginLoadDicomInstanceMode; + + + /** * @brief A 32-bit memory buffer allocated by the core system of Orthanc. * * A memory buffer allocated by the core system of Orthanc. When the @@ -1906,7 +1929,7 @@ sizeof(int32_t) != sizeof(OrthancPluginMetricsType) || sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) || sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) || - sizeof(int32_t) != sizeof(OrthancPluginReceivedInstanceAction)) + sizeof(int32_t) != sizeof(OrthancPluginLoadDicomInstanceMode)) { /* Mismatch in the size of the enumerations */ return 0; @@ -9225,6 +9248,51 @@ return context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV4, ¶ms); } + + typedef struct + { + OrthancPluginDicomInstance** target; + const char* instanceId; + OrthancPluginLoadDicomInstanceMode mode; + } _OrthancPluginLoadDicomInstance; + + /** + * @brief Load a DICOM instance from the Orthanc server. + * + * This function loads a DICOM instance from the content of the + * Orthanc database. The function returns a new pointer to a data + * structure that is managed by the Orthanc core. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param instanceId The Orthanc identifier of the DICOM instance of interest. + * @param mode Flag specifying how to deal with pixel data. + * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance(). + * @ingroup DicomInstance + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginLoadDicomInstance( + OrthancPluginContext* context, + const char* instanceId, + OrthancPluginLoadDicomInstanceMode mode) + { + OrthancPluginDicomInstance* target = NULL; + + _OrthancPluginLoadDicomInstance params; + params.target = ⌖ + params.instanceId = instanceId; + params.mode = mode; + + if (context->InvokeService(context, _OrthancPluginService_LoadDicomInstance, ¶ms) != OrthancPluginErrorCode_Success) + { + /* Error */ + return NULL; + } + else + { + return target; + } + } + + #ifdef __cplusplus } #endif
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c Fri Jun 23 18:01:55 2023 +0200 +++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c Sat Jun 24 12:18:58 2023 +0200 @@ -282,6 +282,60 @@ } +OrthancPluginErrorCode CallbackDicomWeb(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + OrthancPluginLoadDicomInstanceMode mode = OrthancPluginLoadDicomInstanceMode_WholeDicom; + if (request->getCount == 1) + { + if (strcmp(request->getKeys[0], "until-pixel-data") == 0) + { + mode = OrthancPluginLoadDicomInstanceMode_UntilPixelData; + } + else if (strcmp(request->getKeys[0], "empty-pixel-data") == 0) + { + mode = OrthancPluginLoadDicomInstanceMode_EmptyPixelData; + } + else + { + return OrthancPluginErrorCode_ParameterOutOfRange; + } + } + + OrthancPluginDicomInstance* instance = OrthancPluginLoadDicomInstance(context, request->groups[0], mode); + if (instance == NULL) + { + return OrthancPluginErrorCode_UnknownResource; + } + + char* json = OrthancPluginEncodeDicomWebXml(context, + OrthancPluginGetInstanceData(context, instance), + OrthancPluginGetInstanceSize(context, instance), + DicomWebBinaryCallback); + OrthancPluginFreeDicomInstance(context, instance); + + if (json != NULL) + { + OrthancPluginAnswerBuffer(context, output, json, strlen(json), "application/json"); + OrthancPluginFreeString(context, json); + } + else + { + return OrthancPluginErrorCode_InternalError; + } + } + + return OrthancPluginErrorCode_Success; +} + + OrthancPluginErrorCode OnStoredCallback(const OrthancPluginDicomInstance* instance, const char* instanceId) { @@ -511,6 +565,7 @@ OrthancPluginRegisterRestCallback(context, "/forward/(built-in)(/.+)", Callback5); OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5); OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom); + OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/dicom-web", CallbackDicomWeb); OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback); OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);