[med-svn] [Git][med-team/orthanc-dicomweb][upstream] New upstream version 1.18+dfsg
Sebastien Jodogne (@jodogne-guest)
gitlab at salsa.debian.org
Fri Dec 20 09:24:10 GMT 2024
Sebastien Jodogne pushed to branch upstream at Debian Med / orthanc-dicomweb
Commits:
bee0a78e by jodogne-guest at 2024-12-20T09:30:31+01:00
New upstream version 1.18+dfsg
- - - - -
21 changed files:
- .hg_archival.txt
- CITATION.cff
- CMakeLists.txt
- NEWS
- Plugin/Configuration.cpp
- Plugin/Configuration.h
- Plugin/DicomWebClient.cpp
- Plugin/Plugin.cpp
- Plugin/QidoRs.cpp
- Plugin/StowRs.cpp
- Plugin/WadoRs.cpp
- Plugin/WadoRs.h
- Plugin/WadoRsRetrieveFrames.cpp
- Resources/CMake/JavaScriptLibraries.cmake
- Resources/Orthanc/CMake/Compiler.cmake
- Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
- Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
- Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
- TODO
- WebApplication/app.js
- WebApplication/index.html
Changes:
=====================================
.hg_archival.txt
=====================================
@@ -1,6 +1,6 @@
repo: d5f45924411123cfd02d035fd50b8e37536eadef
-node: 599ef9f8918ab8981f6ae33dcadcd734307c324b
-branch: OrthancDicomWeb-1.17
+node: 9535bcd7fa8b6223f03d20921f90c860f8257531
+branch: OrthancDicomWeb-1.18
latesttag: null
-latesttagdistance: 587
-changessincelatesttag: 632
+latesttagdistance: 609
+changessincelatesttag: 658
=====================================
CITATION.cff
=====================================
@@ -10,5 +10,5 @@ authors:
doi: "10.1007/s10278-018-0082-y"
license: "GPL-3.0-or-later"
repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
-version: 1.12.4
-date-released: 2024-06-05
+version: 1.12.5
+date-released: 2024-12-18
=====================================
CMakeLists.txt
=====================================
@@ -23,13 +23,13 @@ cmake_minimum_required(VERSION 2.8)
project(OrthancDicomWeb)
-set(ORTHANC_DICOM_WEB_VERSION "1.17")
+set(ORTHANC_DICOM_WEB_VERSION "1.18")
if (ORTHANC_DICOM_WEB_VERSION STREQUAL "mainline")
set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
else()
- set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.4")
+ set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.5")
set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
endif()
@@ -51,7 +51,6 @@ set(ORTHANC_FRAMEWORK_STATIC OFF CACHE BOOL "If linking against the Orthanc fram
mark_as_advanced(ORTHANC_FRAMEWORK_STATIC)
-set(BUILD_BOOTSTRAP_VUE OFF CACHE BOOL "Compile Bootstrap-Vue from sources")
set(BUILD_BABEL_POLYFILL OFF CACHE BOOL "Retrieve babel-polyfill from npm")
@@ -101,9 +100,7 @@ if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
if (ORTHANC_SDK_VERSION STREQUAL "1.12.1")
include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-1.12.1)
elseif (ORTHANC_SDK_VERSION STREQUAL "framework")
- set(tmp ${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Include/)
- message(${tmp})
- include_directories(${tmp})
+ include_directories(${ORTHANC_FRAMEWORK_ROOT}/../../OrthancServer/Plugins/Include/)
else()
message(FATAL_ERROR "Unsupported version of the Orthanc plugin SDK: ${ORTHANC_SDK_VERSION}")
endif()
=====================================
NEWS
=====================================
@@ -2,6 +2,22 @@ Pending changes in the mainline
===============================
+Version 1.18 (2024-12-18)
+=========================
+
+* Added a "Server" entry in the DICOMweb job content
+* Fixed parsing of numerical values in QIDO-RS response that prevented, among other,
+ the retrieval of "NumberOfStudyRelatedInstances", "NumberOfStudyRelatedSeries",...
+* Fixed non-Latin "PatientName" values that were empty in some QIDO-RS responses
+* Optimized the retrieval of single frame in WADO-RS when no transcoding is required,
+ which greatly improves the download time of multi-frame images in OHIF
+* Optimization when running with an Orthanc that supports the "ExtendedFind" primitive
+* Added support for Orthanc running in "ReadOnly" mode
+* Fix handling of revisions for cached data
+* Removed dependency on bootstrap-vue
+* Upgraded to Bootstrap 5.3.3
+
+
Version 1.17 (2024-06-05)
=========================
@@ -278,7 +294,7 @@ Version 0.4 (2017-07-19)
* WADO-RS client supports quoted Content-Type header in HTTP answers
* Added "Arguments" to WADO-RS and STOW-RS client to handle query arguments in uri
* Using MIME types of DICOM version 2017c in WADO RetrieveFrames
-* Fix issue #53 (DICOMWeb plugin support for "limit" and "offset" parameters in QIDO-RS)
+* Fix issue #53 (DICOMweb plugin support for "limit" and "offset" parameters in QIDO-RS)
* Fix issue #28 (Non-compliant enumerations for "accept" header for WADO RetrieveFrames)
=====================================
Plugin/Configuration.cpp
=====================================
@@ -278,6 +278,8 @@ namespace OrthancPlugins
Orthanc::ResourceType level = Orthanc::StringToResourceType(levels[i].c_str());
const Json::Value& content = configuration[EXTRA_MAIN_DICOM_TAGS][levels[i]];
+ std::set<Orthanc::DicomTag> defaultTags;
+ Orthanc::DicomMap::GetMainDicomTags(defaultTags, level);
if (content.size() > 0)
{
@@ -286,7 +288,10 @@ namespace OrthancPlugins
const std::string& tagName = content[t].asString();
Orthanc::DicomTag tag(0, 0);
OrthancPlugins::ParseTag(tag, tagName);
- Orthanc::DicomMap::AddMainDicomTag(tag, level);
+ if (defaultTags.find(tag) == defaultTags.end()) // don't generate an error if the ExtraMainDicomTags is now a default one
+ {
+ Orthanc::DicomMap::AddMainDicomTag(tag, level);
+ }
}
}
}
@@ -470,11 +475,10 @@ namespace OrthancPlugins
static bool LookupHttpHeader2(std::string& value,
- const HttpClient::HttpHeaders& headers,
+ const HttpHeaders& headers,
const std::string& name)
{
- for (HttpClient::HttpHeaders::const_iterator
- it = headers.begin(); it != headers.end(); ++it)
+ for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
{
if (boost::iequals(it->first, name))
{
@@ -487,7 +491,7 @@ namespace OrthancPlugins
}
- std::string GetBasePublicUrl(const HttpClient::HttpHeaders& headers)
+ std::string GetBasePublicUrl(const HttpHeaders& headers)
{
assert(dicomWebConfiguration_.get() != NULL);
std::string host = dicomWebConfiguration_->GetStringValue("Host", "");
@@ -558,7 +562,7 @@ namespace OrthancPlugins
std::string GetBasePublicUrl(const OrthancPluginHttpRequest* request)
{
- HttpClient::HttpHeaders headers;
+ HttpHeaders headers;
std::string value;
if (LookupHttpHeader(value, request, "forwarded"))
@@ -682,6 +686,10 @@ namespace OrthancPlugins
return GetBooleanValue("EnableMetadataCache", true);
}
+ bool IsReadOnly()
+ {
+ return globalConfiguration_->GetBooleanValue("ReadOnly", false);
+ }
MetadataMode GetMetadataMode(Orthanc::ResourceType level)
{
=====================================
Plugin/Configuration.h
=====================================
@@ -142,5 +142,7 @@ namespace OrthancPlugins
unsigned int GetMetadataWorkerThreadsCount();
bool IsMetadataCacheEnabled();
+
+ bool IsReadOnly();
}
}
=====================================
Plugin/DicomWebClient.cpp
=====================================
@@ -501,26 +501,8 @@ static void CheckStowAnswer(const Json::Value& response,
}
-// static void AddResourceForJobContent(Json::Value resourcesForJobContent /* out */, const char* resourceType, const std::string& resourceId)
static void AddResourceForJobContent(Json::Value& resourcesForJobContent /* out */, Orthanc::ResourceType resourceType, const std::string& resourceId)
{
- // const char* resourceGroup = "Instances";
- // if (resourceType == "Study")
- // {
- // resourceGroup = "Studies";
- // }
- // else if (resourceType == "Series")
- // {
- // resourceGroup = "Series";
- // }
- // else if (resourceType == "Patient")
- // {
- // resourceGroup = "Patients";
- // }
- // else if (resourceType == "Instance")
- // {
- // resourceGroup = "Instances";
- // }
const char* resourceGroup = Orthanc::GetResourceTypeText(resourceType, true, true);
if (!resourcesForJobContent.isMember(resourceGroup))
@@ -616,16 +598,16 @@ private:
Action_Cancel
};
- boost::mutex mutex_;
- std::string serverName_;
- std::vector<std::string> instances_;
- OrthancPlugins::HttpClient::HttpHeaders headers_;
- std::string boundary_;
- size_t position_;
- Action action_;
- size_t networkSize_;
- bool debug_;
- Json::Value resourcesForJobContent_;
+ boost::mutex mutex_;
+ std::string serverName_;
+ std::vector<std::string> instances_;
+ OrthancPlugins::HttpHeaders headers_;
+ std::string boundary_;
+ size_t position_;
+ Action action_;
+ size_t networkSize_;
+ bool debug_;
+ Json::Value resourcesForJobContent_;
bool ReadNextInstance(std::string& dicom,
JobContext& context)
@@ -745,6 +727,7 @@ private:
boost::mutex::scoped_lock lock(that_.mutex_);
context.SetContent("InstancesCount", boost::lexical_cast<std::string>(that_.instances_.size()));
context.SetContent("Resources", that_.GetResourcesForJobContent());
+ context.SetContent("Server", that_.GetServerName());
serverName = that_.serverName_;
startPosition = that_.position_;
@@ -757,7 +740,7 @@ private:
client->AddHeaders(that_.headers_);
}
- OrthancPlugins::HttpClient::HttpHeaders answerHeaders;
+ OrthancPlugins::HttpHeaders answerHeaders;
Json::Value answerBody;
assert(client.get() != NULL);
@@ -839,7 +822,7 @@ private:
public:
StowClientJob(const std::string& serverName,
const std::list<std::string>& instances,
- const OrthancPlugins::HttpClient::HttpHeaders& headers,
+ const OrthancPlugins::HttpHeaders& headers,
const Json::Value& resourcesForJobContent) :
SingleFunctionJob("DicomWebStowClient"),
serverName_(serverName),
@@ -889,6 +872,11 @@ public:
{
return resourcesForJobContent_;
}
+
+ const std::string& GetServerName()
+ {
+ return serverName_;
+ }
};
=====================================
Plugin/Plugin.cpp
=====================================
@@ -42,7 +42,10 @@
#define ORTHANC_CORE_MINIMAL_REVISION 0
static const char* const HAS_DELETE = "HasDelete";
-
+static const char* const SYSTEM_CAPABILITIES = "Capabilities";
+static const char* const SYSTEM_CAPABILITIES_HAS_EXTENDED_FIND = "HasExtendedFind";
+static const char* const READ_ONLY = "ReadOnly";
+bool isReadOnly_ = false;
bool RequestHasKey(const OrthancPluginHttpRequest* request, const char* key)
@@ -256,6 +259,10 @@ void QidoClient(OrthancPluginRestOutput* output,
switch (content.type())
{
+ case Json::intValue:
+ value["Value"] = content;
+ break;
+
case Json::stringValue:
value["Value"] = content.asString();
break;
@@ -467,11 +474,41 @@ static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeTyp
switch (changeType)
{
case OrthancPluginChangeType_OrthancStarted:
+ {
OrthancPlugins::Configuration::LoadDicomWebServers();
- break;
+
+ Json::Value system;
+ if (OrthancPlugins::RestApiGet(system, "/system", false))
+ {
+ bool hasExtendedFind = system.isMember(SYSTEM_CAPABILITIES)
+ && system[SYSTEM_CAPABILITIES].isMember(SYSTEM_CAPABILITIES_HAS_EXTENDED_FIND)
+ && system[SYSTEM_CAPABILITIES][SYSTEM_CAPABILITIES_HAS_EXTENDED_FIND].asBool();
+ if (hasExtendedFind)
+ {
+ LOG(INFO) << "Orthanc supports ExtendedFind.";
+ SetPluginCanUseExtendedFile(true);
+ }
+ else
+ {
+ LOG(WARNING) << "Orthanc does not support ExtendedFind. The DICOMweb plugin will not benefit from some optimizations.";
+ }
+
+ bool isReadOnly = system.isMember(READ_ONLY) && system[READ_ONLY].asBool();
+
+ if (isReadOnly)
+ {
+ LOG(INFO) << "Orthanc is ReadOnly.";
+ SetSystemIsReadOnly(true);
+ }
+ }
+
+ }; break;
case OrthancPluginChangeType_StableSeries:
- CacheSeriesMetadata(resourceId);
+ if (!OrthancPlugins::Configuration::IsReadOnly())
+ {
+ CacheSeriesMetadata(resourceId);
+ }
break;
default:
@@ -564,13 +601,27 @@ extern "C"
LOG(WARNING) << "URI to the DICOMweb REST API: " << root;
- OrthancPlugins::ChunkedRestRegistration<
- SearchForStudies /* TODO => Rename as QIDO-RS */,
- OrthancPlugins::StowServer::PostCallback>::Apply(root + "studies");
- OrthancPlugins::ChunkedRestRegistration<
- RetrieveDicomStudy /* TODO => Rename as WADO-RS */,
- OrthancPlugins::StowServer::PostCallback>::Apply(root + "studies/([^/]*)");
+ if (!OrthancPlugins::Configuration::IsReadOnly())
+ {
+ OrthancPlugins::ChunkedRestRegistration<
+ SearchForStudies /* TODO => Rename as QIDO-RS */,
+ OrthancPlugins::StowServer::PostCallback>::Apply(root + "studies");
+
+ OrthancPlugins::ChunkedRestRegistration<
+ RetrieveDicomStudy /* TODO => Rename as WADO-RS */,
+ OrthancPlugins::StowServer::PostCallback>::Apply(root + "studies/([^/]*)");
+ }
+ else
+ {
+ LOG(WARNING) << "READ-ONLY SYSTEM: deactivating STOW-RS routes";
+
+ OrthancPlugins::ChunkedRestRegistration<
+ SearchForStudies /* TODO => Rename as QIDO-RS */>::Apply(root + "studies");
+
+ OrthancPlugins::ChunkedRestRegistration<
+ RetrieveDicomStudy /* TODO => Rename as WADO-RS */>::Apply(root + "studies/([^/]*)");
+ }
OrthancPlugins::RegisterRestCallback<SearchForInstances>(root + "instances", true);
OrthancPlugins::RegisterRestCallback<SearchForSeries>(root + "series", true);
@@ -588,10 +639,19 @@ extern "C"
OrthancPlugins::RegisterRestCallback<ListServers>(root + "servers", true);
OrthancPlugins::RegisterRestCallback<ListServerOperations>(root + "servers/([^/]*)", true);
- OrthancPlugins::RegisterRestCallback<StowClient>(root + "servers/([^/]*)/stow", true);
+
+ if (!OrthancPlugins::Configuration::IsReadOnly())
+ {
+ OrthancPlugins::RegisterRestCallback<RetrieveFromServer>(root + "servers/([^/]*)/retrieve", true);
+ }
+ else
+ {
+ LOG(WARNING) << "READ-ONLY SYSTEM: deactivating 'servers/../retrieve' route";
+ }
+
OrthancPlugins::RegisterRestCallback<WadoRetrieveClient>(root + "servers/([^/]*)/wado", true);
+ OrthancPlugins::RegisterRestCallback<StowClient>(root + "servers/([^/]*)/stow", true);
OrthancPlugins::RegisterRestCallback<GetFromServer>(root + "servers/([^/]*)/get", true);
- OrthancPlugins::RegisterRestCallback<RetrieveFromServer>(root + "servers/([^/]*)/retrieve", true);
OrthancPlugins::RegisterRestCallback<QidoClient>(root + "servers/([^/]*)/qido", true);
OrthancPlugins::RegisterRestCallback<DeleteClient>(root + "servers/([^/]*)/delete", true);
@@ -606,7 +666,10 @@ extern "C"
OrthancPlugins::RegisterRestCallback<RetrieveInstanceRendered>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/rendered", true);
OrthancPlugins::RegisterRestCallback<RetrieveFrameRendered>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)/rendered", true);
- OrthancPlugins::RegisterRestCallback<UpdateSeriesMetadataCache>("/studies/([^/]*)/update-dicomweb-cache", true);
+ if (!OrthancPlugins::Configuration::IsReadOnly())
+ {
+ OrthancPlugins::RegisterRestCallback<UpdateSeriesMetadataCache>("/studies/([^/]*)/update-dicomweb-cache", true);
+ }
OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
@@ -644,7 +707,7 @@ extern "C"
OrthancPlugins::SetRootUri(ORTHANC_DICOM_WEB_NAME, uri.c_str());
std::string publicUrlRoot = OrthancPlugins::Configuration::GetPublicRoot();
- LOG(WARNING) << "DICOMWeb PublicRoot: " << publicUrlRoot;
+ LOG(WARNING) << "DICOMweb public root: " << publicUrlRoot;
}
else
{
=====================================
Plugin/QidoRs.cpp
=====================================
@@ -132,6 +132,16 @@ namespace
for (uint32_t i = 0; i < request->getCount; i++)
{
std::string key(request->getKeys[i]);
+ if (key.empty())
+ {
+ /**
+ * This case can happen in Weasis, which uses the following
+ * URL if doing a basic QIDO-RS request (note the "?&"):
+ * http://localhost:8042/dicom-web/studies?&includefield=00080020,00080030,00080050,00080061,00080090,00081030,00100010,00100020,00100021,00100030,00100040,0020000D,00200010&limit=10&offset=0
+ **/
+ continue;
+ }
+
std::string value(request->getValues[i]);
args += " [" + key + "=" + value + "]";
@@ -423,6 +433,9 @@ static void ApplyMatcher(OrthancPluginRestOutput* output,
Orthanc::DicomMap target;
+ // since we are populating the target with values from JSON, all string are actually UTF-8
+ target.SetValue(Orthanc::DICOM_TAG_SPECIFIC_CHARACTER_SET, "ISO_IR 192", false);
+
matcher.ExtractFields(target, source, wadoBasePublicUrl, level);
writer.AddOrthancMap(target);
}
=====================================
Plugin/StowRs.cpp
=====================================
@@ -187,9 +187,9 @@ namespace OrthancPlugins
MemoryBuffer tmp;
// make sure to forward the auth headers in the request that is sent to Orthanc (to allow usage of the auth plugin)
- // since we do not know which header is being used, we include all the headers from the Stow-RS request (headers_), replace
+ // since we do not know which header is being used, we include all the headers from the STOW-RS request (headers_), replace
// the "content-disposition" + "content-type" that are rebuilt from the multi-part message and remove the headers that might
- // be mi-interpreted by Orthanc core (like "content-length" that is actually the "content-length" from the whole Stow-RS request, not the length of this file)
+ // be mi-interpreted by Orthanc core (like "content-length" that is actually the "content-length" from the whole STOW-RS request, not the length of this file)
Orthanc::MultipartStreamReader::HttpHeaders mergedHeaders = headers_;
Orthanc::MultipartStreamReader::HttpHeaders::iterator foundContentLength = mergedHeaders.find("content-length");
if (foundContentLength != mergedHeaders.end())
=====================================
Plugin/WadoRs.cpp
=====================================
@@ -50,6 +50,28 @@ static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
static std::string instancesMainDicomTagsList;
static boost::mutex mainDicomTagsListMutex;
+static bool pluginCanUseExtendedFind_ = false;
+static bool isSystemReadOnly_ = false;
+
+void SetPluginCanUseExtendedFile(bool enable)
+{
+ pluginCanUseExtendedFind_ = enable;
+}
+
+bool CanUseExtendedFile()
+{
+ return pluginCanUseExtendedFind_;
+}
+
+void SetSystemIsReadOnly(bool isReadOnly)
+{
+ isSystemReadOnly_ = isReadOnly;
+}
+
+bool IsSystemReadOnly()
+{
+ return isSystemReadOnly_;
+}
static std::string GetResourceUri(Orthanc::ResourceType level,
const std::string& publicId)
@@ -900,7 +922,15 @@ static void WriteInstanceMetadata(OrthancPlugins::DicomWebFormatter::HttpWriter&
false /* JSON */, OrthancPluginDicomWebBinaryMode_Ignore, "");
}
- buffer.RestApiPut("/instances/" + orthancId + "/attachments/4444", dicomweb, false);
+ try
+ {
+ buffer.RestApiPut("/instances/" + orthancId + "/attachments/4444", dicomweb, false);
+ }
+ catch (Orthanc::OrthancException& e)
+ {
+ // An exception might occur if another writer has concurrently created the attachment, ignore this case
+ }
+
writer.AddDicomWebInstanceSerializedJson(dicomweb.c_str(), dicomweb.size());
}
}
@@ -1348,15 +1378,13 @@ void RetrieveSeriesMetadataInternal(std::set<std::string>& instancesIds,
const std::string& wadoBase)
{
const unsigned int workersCount = OrthancPlugins::Configuration::GetMetadataWorkerThreadsCount();
- const bool oneLargeQuery = false; // we keep this code here for future use once we'll have optimized Orthanc API /series/.../instances?full to minimize the SQL queries
- // right now, it is faster to call /instances/..?full in each worker but, later, it should be more efficient with a large SQL query in Orthanc
if (workersCount > 1 && mode == OrthancPlugins::MetadataMode_Full)
{
ChildrenMainDicomMaps instancesDicomMaps;
std::string seriesDicomUid;
- if (oneLargeQuery)
+ if (CanUseExtendedFile()) // in this case, /series/.../instances?full has been optimized to minimize the SQL queries
{
GetChildrenMainDicomTags(instancesDicomMaps, seriesDicomUid, Orthanc::ResourceType_Series, seriesOrthancId);
for (ChildrenMainDicomMaps::const_iterator it = instancesDicomMaps.begin(); it != instancesDicomMaps.end(); ++it)
@@ -1382,7 +1410,7 @@ void RetrieveSeriesMetadataInternal(std::set<std::string>& instancesIds,
instancesWorkers.push_back(boost::shared_ptr<boost::thread>(new boost::thread(InstanceWorkerThread, threadData)));
}
- if (oneLargeQuery)
+ if (CanUseExtendedFile()) // we must correct the bulkRoot
{
for (ChildrenMainDicomMaps::const_iterator i = instancesDicomMaps.begin(); i != instancesDicomMaps.end(); ++i)
{
@@ -1448,20 +1476,39 @@ void CacheSeriesMetadataInternal(std::string& serializedSeriesMetadata,
RetrieveSeriesMetadataInternal(instancesIds, writer, cache, OrthancPlugins::MetadataMode_Full, false /* isXml */, seriesOrthancId, studyInstanceUid, seriesInstanceUid, WADO_BASE_PLACEHOLDER);
writer.CloseAndGetJsonOutput(serializedSeriesMetadata);
- // save in attachments for future use
- Orthanc::IBufferCompressor::Compress(compressedSeriesMetadata, compressor, serializedSeriesMetadata);
- std::string instancesMd5;
- Orthanc::Toolbox::ComputeMD5(instancesMd5, instancesIds);
+ if (!IsSystemReadOnly())
+ {
+ // save in attachments for future use
+ Orthanc::IBufferCompressor::Compress(compressedSeriesMetadata, compressor, serializedSeriesMetadata);
+ std::string instancesMd5;
+ Orthanc::Toolbox::ComputeMD5(instancesMd5, instancesIds);
+
+ std::string cacheContent = "2;" + instancesMd5 + ";" + compressedSeriesMetadata;
- std::string cacheContent = "2;" + instancesMd5 + ";" + compressedSeriesMetadata;
+ Json::Value putResult;
+ std::string attachmentUrl = "/series/" + seriesOrthancId + "/attachments/" + SERIES_METADATA_ATTACHMENT_ID;
- Json::Value putResult;
- std::string attachmentUrl = "/series/" + seriesOrthancId + "/attachments/" + SERIES_METADATA_ATTACHMENT_ID;
- if (!OrthancPlugins::RestApiPut(putResult, attachmentUrl, cacheContent, false))
- {
- LOG(WARNING) << "DicomWEB: failed to write series metadata attachment";
- }
+ OrthancPlugins::RestApiClient client;
+ client.SetMethod(OrthancPluginHttpMethod_Get);
+ client.SetPath(attachmentUrl);
+ std::string etag;
+ bool hasRevision = (client.Execute() &&
+ client.LookupAnswerHeader(etag, "etag"));
+
+ client.SetMethod(OrthancPluginHttpMethod_Put);
+ client.SwapRequestBody(cacheContent);
+
+ if (hasRevision)
+ {
+ client.AddRequestHeader("If-Match", etag);
+ }
+
+ if (!client.Execute())
+ {
+ LOG(WARNING) << "DicomWEB: failed to write series metadata attachment";
+ }
+ }
}
void CacheSeriesMetadata(const std::string& seriesOrthancId)
=====================================
Plugin/WadoRs.h
=====================================
@@ -102,4 +102,8 @@ void RetrieveStudyRendered(OrthancPluginRestOutput* output,
const char* url,
const OrthancPluginHttpRequest* request);
-void SetPluginCanDownloadTranscodedFile(bool enable);
\ No newline at end of file
+void SetPluginCanDownloadTranscodedFile(bool enable);
+
+void SetPluginCanUseExtendedFile(bool enable);
+
+void SetSystemIsReadOnly(bool isReadOnly);
\ No newline at end of file
=====================================
Plugin/WadoRsRetrieveFrames.cpp
=====================================
@@ -433,6 +433,43 @@ static void AnswerFrames(OrthancPluginRestOutput* output,
}
+static void AnswerFrame(OrthancPluginRestOutput* output,
+ const OrthancPluginHttpRequest* request,
+ const OrthancPlugins::MemoryBuffer& instanceContent,
+ const std::string& studyInstanceUid,
+ const std::string& seriesInstanceUid,
+ const std::string& sopInstanceUid,
+ unsigned int frame,
+ Orthanc::DicomTransferSyntax outputSyntax)
+{
+ if (OrthancPluginStartMultipartAnswer(
+ OrthancPlugins::GetGlobalContext(),
+ output, "related", GetMimeType(outputSyntax)) != OrthancPluginErrorCode_Success)
+ {
+ throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin,
+ "Cannot start a multipart answer");
+ }
+
+ OrthancPluginErrorCode error;
+
+#if HAS_SEND_MULTIPART_ITEM_2 == 1
+ const std::string base = OrthancPlugins::Configuration::GetBasePublicUrl(request);
+ std::string location = (
+ OrthancPlugins::Configuration::GetWadoUrl(base, studyInstanceUid, seriesInstanceUid, sopInstanceUid) +
+ "frames/" + boost::lexical_cast<std::string>(frame + 1));
+ const char *keys[] = { "Content-Location" };
+ const char *values[] = { location.c_str() };
+ error = OrthancPluginSendMultipartItem2(OrthancPlugins::GetGlobalContext(), output, instanceContent.GetData(), instanceContent.GetSize(), 1, keys, values);
+#else
+ error = OrthancPluginSendMultipartItem(OrthancPlugins::GetGlobalContext(), instanceContent.GetData(), instanceContent.GetSize(), size);
+#endif
+
+ if (error != OrthancPluginErrorCode_Success)
+ {
+ throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+ }
+}
+
static void RetrieveFrames(OrthancPluginRestOutput* output,
const OrthancPluginHttpRequest* request,
bool allFrames,
@@ -466,7 +503,7 @@ static void RetrieveFrames(OrthancPluginRestOutput* output,
std::string currentSyntaxString;
if (!OrthancPlugins::RestApiGetString(currentSyntaxString, "/instances/" + orthancId + "/metadata/TransferSyntax", false))
{
- throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMWeb: Unable to get TransferSyntax for instance " + orthancId);
+ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMweb: Unable to get TransferSyntax for instance " + orthancId);
}
if (!Orthanc::LookupTransferSyntax(currentSyntax, currentSyntaxString))
@@ -481,7 +518,7 @@ static void RetrieveFrames(OrthancPluginRestOutput* output,
// note: these 2 syntaxes are not supposed to be used in retrieve frames
// according to https://dicom.nema.org/MEDICAL/dicom/2019a/output/chtml/part18/chapter_6.html#table_6.1.1.8-3b
// "The Implicit VR Little Endian (1.2.840.10008.1.2), and Explicit VR Big Endian (1.2.840.10008.1.2.2) transfer syntaxes shall not be used with Web Services."
- LOG(INFO) << "The file is in a transfer syntax " << currentSyntaxString << " that is not allowed by the DICOMWeb standard -> it will be transcoded to Little Endian Explicit";
+ LOG(INFO) << "The file is in a transfer syntax " << currentSyntaxString << " that is not allowed by the DICOMweb standard -> it will be transcoded to Little Endian Explicit";
targetSyntax = Orthanc::DicomTransferSyntax_LittleEndianExplicit;
}
@@ -497,15 +534,29 @@ static void RetrieveFrames(OrthancPluginRestOutput* output,
{
if (!content.RestApiGet("/instances/" + orthancId + "/file?transcode=" + Orthanc::GetTransferSyntaxUid(targetSyntax), false))
{
- throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMWeb: Unable to get transcoded file for instance " + orthancId);
+ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMweb: Unable to get transcoded file for instance " + orthancId);
}
+
+ // TODO-OPTI: this takes a huge amount of time; e.g: 1.5s for a 600MB file while the DicomInstance usually already exists in the Orthanc core
+ // call /instances/../frames/../transcoded (to be implemented in future Orthanc release)
instance.reset(new OrthancPlugins::DicomInstance(content.GetData(), content.GetSize()));
}
- else // pre 1.12.2 code (or no transcoding needed)
+ else if (!allFrames && frames.size() == 1 && !transcodeThisInstance) // no transcoding needed, let's retrieve the raw frame directly from the core to avoid Orthanc to recreate a DicomInstance for every frame
+ {
+ if (!content.RestApiGet("/instances/" + orthancId + "/frames/" + boost::lexical_cast<std::string>(frames.front()) + "/raw", false))
+ {
+ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMweb: Unable to get file for instance " + orthancId);
+ }
+
+ AnswerFrame(output, request, content, studyInstanceUid, seriesInstanceUid,
+ sopInstanceUid, frames.front(), targetSyntax);
+ return;
+ }
+ else
{
if (!content.RestApiGet("/instances/" + orthancId + "/file", false))
{
- throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMWeb: Unable to get file for instance " + orthancId);
+ throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "DICOMweb: Unable to get file for instance " + orthancId);
}
instance.reset(new OrthancPlugins::DicomInstance(content.GetData(), content.GetSize()));
=====================================
Resources/CMake/JavaScriptLibraries.cmake
=====================================
@@ -19,95 +19,29 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-set(BASE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/dicom-web")
+set(BASE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads")
DownloadPackage(
- "da0189f7c33bf9f652ea65401e0a3dc9"
- "${BASE_URL}/bootstrap-4.3.1.zip"
- "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1")
+ "102a4386a022f26a3b604e3852fffba8"
+ "${BASE_URL}/bootstrap-5.3.3.zip"
+ "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3")
DownloadPackage(
"8242afdc5bd44105d9dc9e6535315484"
- "${BASE_URL}/vuejs-2.6.10.tar.gz"
+ "${BASE_URL}/dicom-web/vuejs-2.6.10.tar.gz"
"${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10")
DownloadPackage(
"3e2b4e1522661f7fcf8ad49cb933296c"
- "${BASE_URL}/axios-0.19.0.tar.gz"
+ "${BASE_URL}/dicom-web/axios-0.19.0.tar.gz"
"${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0")
DownloadPackage(
"a6145901f233f7d54165d8ade779082e"
- "${BASE_URL}/Font-Awesome-4.7.0.tar.gz"
+ "${BASE_URL}/dicom-web/Font-Awesome-4.7.0.tar.gz"
"${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0")
-set(BOOTSTRAP_VUE_SOURCES_DIR ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-vue-2.0.0-rc.24)
-
-if (BUILD_BOOTSTRAP_VUE OR
- BUILD_BABEL_POLYFILL)
- find_program(NPM_EXECUTABLE npm)
- if (${NPM_EXECUTABLE} MATCHES "NPM_EXECUTABLE-NOTFOUND")
- message(FATAL_ERROR "Please install the 'npm' standard command-line tool")
- endif()
-endif()
-
-if (BUILD_BOOTSTRAP_VUE)
- DownloadPackage(
- "36ab31495ab94162e159619532e8def5"
- "${BASE_URL}/bootstrap-vue-2.0.0-rc.24.tar.gz"
- "${BOOTSTRAP_VUE_SOURCES_DIR}")
-
- if (NOT IS_DIRECTORY "${BOOTSTRAP_VUE_SOURCES_DIR}/node_modules")
- execute_process(
- COMMAND ${NPM_EXECUTABLE} install
- WORKING_DIRECTORY ${BOOTSTRAP_VUE_SOURCES_DIR}
- RESULT_VARIABLE Failure
- OUTPUT_QUIET
- )
-
- if (Failure)
- message(FATAL_ERROR "Error while running 'npm install' on Bootstrap-Vue")
- endif()
- endif()
-
- if (NOT IS_DIRECTORY "${BOOTSTRAP_VUE_SOURCES_DIR}/dist")
- execute_process(
- COMMAND ${NPM_EXECUTABLE} run build
- WORKING_DIRECTORY ${BOOTSTRAP_VUE_SOURCES_DIR}
- RESULT_VARIABLE Failure
- OUTPUT_QUIET
- )
-
- if (Failure)
- message(FATAL_ERROR "Error while running 'npm build' on Bootstrap-Vue")
- endif()
- endif()
-
-else()
-
- ##
- ## Generation of the precompiled Bootstrap-Vue package:
- ##
- ## Possibility 1 (build from sources):
- ## $ cmake -DBUILD_BOOTSTRAP_VUE=ON .
- ## $ tar cvfz bootstrap-vue-2.0.0-rc.24-dist.tar.gz bootstrap-vue-2.0.0-rc.24/dist/
- ##
- ## Possibility 2 (download from CDN):
- ## $ mkdir /tmp/i && cd /tmp/i
- ## $ wget -r --no-parent https://unpkg.com/bootstrap-vue@2.0.0-rc.24/dist/
- ## $ mv unpkg.com/bootstrap-vue at 2.0.0-rc.24/ bootstrap-vue-2.0.0-rc.24
- ## $ rm bootstrap-vue-2.0.0-rc.24/dist/index.html
- ## $ tar cvfz bootstrap-vue-2.0.0-rc.24-dist.tar.gz bootstrap-vue-2.0.0-rc.24/dist/
-
- DownloadPackage(
- "ba0e67b1f0b4ce64e072b42b17f6c578"
- "${BASE_URL}/bootstrap-vue-2.0.0-rc.24-dist.tar.gz"
- "${BOOTSTRAP_VUE_SOURCES_DIR}")
-
-endif()
-
-
if (BUILD_BABEL_POLYFILL)
set(BABEL_POLYFILL_SOURCES_DIR ${CMAKE_CURRENT_BINARY_DIR}/node_modules/babel-polyfill/dist)
@@ -120,7 +54,7 @@ if (BUILD_BABEL_POLYFILL)
)
if (Failure)
- message(FATAL_ERROR "Error while running 'npm install' on Bootstrap-Vue")
+ message(FATAL_ERROR "Error while running 'npm install' on babel-polyfill")
endif()
endif()
else()
@@ -130,7 +64,7 @@ else()
set(BABEL_POLYFILL_SOURCES_DIR ${CMAKE_CURRENT_BINARY_DIR})
DownloadCompressedFile(
"49f7bad4176d715ce145e75c903988ef"
- "${BASE_URL}/babel-polyfill-6.26.0.min.js.gz"
+ "${BASE_URL}/dicom-web/babel-polyfill-6.26.0.min.js.gz"
"${CMAKE_CURRENT_BINARY_DIR}/polyfill.min.js")
endif()
@@ -141,22 +75,18 @@ file(MAKE_DIRECTORY ${JAVASCRIPT_LIBS_DIR})
file(COPY
${BABEL_POLYFILL_SOURCES_DIR}/polyfill.min.js
- ${BOOTSTRAP_VUE_SOURCES_DIR}/dist/bootstrap-vue.min.js
- ${BOOTSTRAP_VUE_SOURCES_DIR}/dist/bootstrap-vue.min.js.map
${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.js
${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.map
- ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/js/bootstrap.min.js
+ ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3/dist/js/bootstrap.min.js
${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js
DESTINATION
${JAVASCRIPT_LIBS_DIR}/js
)
file(COPY
- ${BOOTSTRAP_VUE_SOURCES_DIR}/dist/bootstrap-vue.min.css
- ${BOOTSTRAP_VUE_SOURCES_DIR}/dist/bootstrap-vue.min.css.map
${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/css/font-awesome.min.css
- ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css
- ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css.map
+ ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3/dist/css/bootstrap.min.css
+ ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-5.3.3/dist/css/bootstrap.min.css.map
DESTINATION
${JAVASCRIPT_LIBS_DIR}/css
)
=====================================
Resources/Orthanc/CMake/Compiler.cmake
=====================================
@@ -232,6 +232,10 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
endif()
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+
+ # fix this error that appears with recent compilers on MacOS: boost/mpl/aux_/integral_wrapper.hpp:73:31: error: integer value -1 is outside the valid range of values [0, 3] for this enumeration type [-Wenum-constexpr-conversion]
+ SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-enum-constexpr-conversion")
+
add_definitions(
-D_XOPEN_SOURCE=1
)
=====================================
Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
=====================================
@@ -165,6 +165,8 @@ if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
set(ORTHANC_FRAMEWORK_MD5 "975f5bf2142c22cb1777b4f6a0a614c5")
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.4")
set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73")
+ elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.5")
+ set(ORTHANC_FRAMEWORK_MD5 "5bb69f092981fdcfc11dec0a0f9a7db3")
# Below this point are development snapshots that were used to
# release some plugin, before an official release of the Orthanc
=====================================
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
=====================================
@@ -334,9 +334,9 @@ namespace OrthancPlugins
std::vector<const char*> headersValues_;
public:
- explicit PluginHttpHeaders(const std::map<std::string, std::string>& httpHeaders)
+ explicit PluginHttpHeaders(const HttpHeaders& httpHeaders)
{
- for (std::map<std::string, std::string>::const_iterator
+ for (HttpHeaders::const_iterator
it = httpHeaders.begin(); it != httpHeaders.end(); ++it)
{
headersKeys_.push_back(it->first.c_str());
@@ -361,7 +361,7 @@ namespace OrthancPlugins
};
bool MemoryBuffer::RestApiGet(const std::string& uri,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins)
{
Clear();
@@ -400,7 +400,7 @@ namespace OrthancPlugins
bool MemoryBuffer::RestApiPost(const std::string& uri,
const void* body,
size_t bodySize,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins)
{
MemoryBuffer answerHeaders;
@@ -422,7 +422,7 @@ namespace OrthancPlugins
bool MemoryBuffer::RestApiPost(const std::string& uri,
const Json::Value& body,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins)
{
std::string s;
@@ -1490,7 +1490,7 @@ namespace OrthancPlugins
bool RestApiGetString(std::string& result,
const std::string& uri,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins)
{
MemoryBuffer answer;
@@ -1508,7 +1508,7 @@ namespace OrthancPlugins
bool RestApiGet(Json::Value& result,
const std::string& uri,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins)
{
MemoryBuffer answer;
@@ -1598,7 +1598,7 @@ namespace OrthancPlugins
bool RestApiPost(Json::Value& result,
const std::string& uri,
const Json::Value& body,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins)
{
MemoryBuffer answer;
@@ -1963,7 +1963,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoGet(MemoryBuffer& target,
size_t index,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
if (index >= index_.size())
{
@@ -1994,7 +1994,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoGet(MemoryBuffer& target,
const std::string& name,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
size_t index;
return (LookupName(index, name) &&
@@ -2005,7 +2005,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoGet(Json::Value& target,
size_t index,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
MemoryBuffer buffer;
@@ -2024,7 +2024,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoGet(Json::Value& target,
const std::string& name,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
MemoryBuffer buffer;
@@ -2044,7 +2044,7 @@ namespace OrthancPlugins
const std::string& name,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
size_t index;
return (LookupName(index, name) &&
@@ -2056,7 +2056,7 @@ namespace OrthancPlugins
size_t index,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
MemoryBuffer buffer;
@@ -2076,7 +2076,7 @@ namespace OrthancPlugins
const std::string& name,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
MemoryBuffer buffer;
@@ -2096,7 +2096,7 @@ namespace OrthancPlugins
size_t index,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
if (index >= index_.size())
{
@@ -2133,7 +2133,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoPut(size_t index,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
if (index >= index_.size())
{
@@ -2169,7 +2169,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoPut(const std::string& name,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
size_t index;
return (LookupName(index, name) &&
@@ -2179,7 +2179,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoDelete(size_t index,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
if (index >= index_.size())
{
@@ -2208,7 +2208,7 @@ namespace OrthancPlugins
bool OrthancPeers::DoDelete(const std::string& name,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const
+ const HttpHeaders& headers) const
{
size_t index;
return (LookupName(index, name) &&
@@ -2923,12 +2923,12 @@ namespace OrthancPlugins
std::vector<const char*> headersValues_;
public:
- HeadersWrapper(const HttpClient::HttpHeaders& headers)
+ HeadersWrapper(const HttpHeaders& headers)
{
headersKeys_.reserve(headers.size());
headersValues_.reserve(headers.size());
- for (HttpClient::HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
+ for (HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
{
headersKeys_.push_back(it->first.c_str());
headersValues_.push_back(it->second.c_str());
@@ -3076,11 +3076,11 @@ namespace OrthancPlugins
class MemoryAnswer : public HttpClient::IAnswer
{
private:
- HttpClient::HttpHeaders headers_;
- ChunkedBuffer body_;
+ HttpHeaders headers_;
+ ChunkedBuffer body_;
public:
- const HttpClient::HttpHeaders& GetHeaders() const
+ const HttpHeaders& GetHeaders() const
{
return headers_;
}
@@ -3168,6 +3168,35 @@ namespace OrthancPlugins
#endif
+ static void DecodeHttpHeaders(HttpHeaders& target,
+ const MemoryBuffer& source)
+ {
+ Json::Value v;
+ source.ToJson(v);
+
+ if (v.type() != Json::objectValue)
+ {
+ ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+ }
+
+ Json::Value::Members members = v.getMemberNames();
+ target.clear();
+
+ for (size_t i = 0; i < members.size(); i++)
+ {
+ const Json::Value& h = v[members[i]];
+ if (h.type() != Json::stringValue)
+ {
+ ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+ }
+ else
+ {
+ target[members[i]] = h.asString();
+ }
+ }
+ }
+
+
void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus,
HttpHeaders& answerHeaders,
std::string& answerBody,
@@ -3208,30 +3237,7 @@ namespace OrthancPlugins
ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
}
- Json::Value v;
- answerHeadersBuffer.ToJson(v);
-
- if (v.type() != Json::objectValue)
- {
- ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
- }
-
- Json::Value::Members members = v.getMemberNames();
- answerHeaders.clear();
-
- for (size_t i = 0; i < members.size(); i++)
- {
- const Json::Value& h = v[members[i]];
- if (h.type() != Json::stringValue)
- {
- ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
- }
- else
- {
- answerHeaders[members[i]] = h.asString();
- }
- }
-
+ DecodeHttpHeaders(answerHeaders, answerHeadersBuffer);
answerBodyBuffer.ToString(answerBody);
}
@@ -4061,7 +4067,7 @@ namespace OrthancPlugins
}
#endif
- void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request)
+ void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request)
{
result.clear();
@@ -4114,4 +4120,135 @@ namespace OrthancPlugins
SetPluginProperty(pluginIdentifier, _OrthancPluginProperty_OrthancExplorer, javascript);
#endif
}
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+ RestApiClient::RestApiClient() :
+ method_(OrthancPluginHttpMethod_Get),
+ path_("/"),
+ afterPlugins_(false),
+ httpStatus_(0)
+ {
+ }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+ void RestApiClient::AddRequestHeader(const std::string& key,
+ const std::string& value)
+ {
+ if (requestHeaders_.find(key) == requestHeaders_.end())
+ {
+ requestHeaders_[key] = value;
+ }
+ else
+ {
+ ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+ }
+ }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+ bool RestApiClient::Execute()
+ {
+ if (requestBody_.size() > 0xffffffffu)
+ {
+ ORTHANC_PLUGINS_LOG_ERROR("Cannot handle body size > 4GB");
+ ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+ }
+
+ PluginHttpHeaders converted(requestHeaders_);
+
+ MemoryBuffer body;
+ MemoryBuffer headers;
+
+ OrthancPluginErrorCode code = OrthancPluginCallRestApi(GetGlobalContext(), *body, *headers, &httpStatus_, method_, path_.c_str(),
+ requestHeaders_.size(), converted.GetKeys(), converted.GetValues(),
+ requestBody_.c_str(), requestBody_.size(), afterPlugins_ ? 1 : 0);
+
+ answerHeaders_.clear();
+ answerBody_.clear();
+
+ if (code == OrthancPluginErrorCode_Success)
+ {
+ if (httpStatus_ == 0)
+ {
+ ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+ }
+
+ DecodeHttpHeaders(answerHeaders_, headers);
+ body.ToString(answerBody_);
+ return true;
+ }
+ else
+ {
+ if (code == OrthancPluginErrorCode_UnknownResource ||
+ code == OrthancPluginErrorCode_InexistentItem)
+ {
+ httpStatus_ = 404;
+ return false;
+ }
+ else
+ {
+ ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+ }
+ }
+ }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+ uint16_t RestApiClient::GetHttpStatus() const
+ {
+ if (httpStatus_ == 0)
+ {
+ ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+ }
+ else
+ {
+ return httpStatus_;
+ }
+ }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+ bool RestApiClient::LookupAnswerHeader(std::string& value,
+ const std::string& key) const
+ {
+ if (httpStatus_ == 0)
+ {
+ ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+ }
+ else
+ {
+ HttpHeaders::const_iterator found = answerHeaders_.find(key);
+ if (found == answerHeaders_.end())
+ {
+ return false;
+ }
+ else
+ {
+ value = found->second;
+ return true;
+ }
+ }
+ }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+ const std::string& RestApiClient::GetAnswerBody() const
+ {
+ if (httpStatus_ == 0)
+ {
+ ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+ }
+ else
+ {
+ return answerBody_;
+ }
+ }
+#endif
}
=====================================
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
=====================================
@@ -170,6 +170,8 @@
namespace OrthancPlugins
{
+ typedef std::map<std::string, std::string> HttpHeaders;
+
typedef void (*RestCallback) (OrthancPluginRestOutput* output,
const char* url,
const OrthancPluginHttpRequest* request);
@@ -257,7 +259,7 @@ namespace OrthancPlugins
bool applyPlugins);
bool RestApiGet(const std::string& uri,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins);
bool RestApiPost(const std::string& uri,
@@ -277,13 +279,13 @@ namespace OrthancPlugins
#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
bool RestApiPost(const std::string& uri,
const Json::Value& body,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins);
bool RestApiPost(const std::string& uri,
const void* body,
size_t bodySize,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins);
#endif
@@ -581,7 +583,7 @@ namespace OrthancPlugins
bool RestApiGet(Json::Value& result,
const std::string& uri,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins);
bool RestApiGetString(std::string& result,
@@ -590,7 +592,7 @@ namespace OrthancPlugins
bool RestApiGetString(std::string& result,
const std::string& uri,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins);
bool RestApiPost(std::string& result,
@@ -609,7 +611,7 @@ namespace OrthancPlugins
bool RestApiPost(Json::Value& result,
const std::string& uri,
const Json::Value& body,
- const std::map<std::string, std::string>& httpHeaders,
+ const HttpHeaders& httpHeaders,
bool applyPlugins);
#endif
@@ -829,64 +831,64 @@ namespace OrthancPlugins
bool DoGet(MemoryBuffer& target,
size_t index,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoGet(MemoryBuffer& target,
const std::string& name,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoGet(Json::Value& target,
size_t index,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoGet(Json::Value& target,
const std::string& name,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoPost(MemoryBuffer& target,
size_t index,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoPost(MemoryBuffer& target,
const std::string& name,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoPost(Json::Value& target,
size_t index,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoPost(Json::Value& target,
const std::string& name,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoPut(size_t index,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoPut(const std::string& name,
const std::string& uri,
const std::string& body,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoDelete(size_t index,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
bool DoDelete(const std::string& name,
const std::string& uri,
- const std::map<std::string, std::string>& headers) const;
+ const HttpHeaders& headers) const;
};
#endif
@@ -996,8 +998,6 @@ namespace OrthancPlugins
class HttpClient : public boost::noncopyable
{
public:
- typedef std::map<std::string, std::string> HttpHeaders;
-
class IRequestBody : public boost::noncopyable
{
public:
@@ -1397,7 +1397,7 @@ namespace OrthancPlugins
};
// helper method to convert Http headers from the plugin SDK to a std::map
-void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPluginHttpRequest* request);
+void GetHttpHeaders(HttpHeaders& result, const OrthancPluginHttpRequest* request);
#if HAS_ORTHANC_PLUGIN_WEBDAV == 1
class IWebDavCollection : public boost::noncopyable
@@ -1508,4 +1508,88 @@ void GetHttpHeaders(std::map<std::string, std::string>& result, const OrthancPlu
void ExtendOrthancExplorer(const std::string& pluginIdentifier,
const std::string& javascript);
+
+
+#if HAS_ORTHANC_PLUGIN_GENERIC_CALL_REST_API == 1
+ class RestApiClient : public boost::noncopyable
+ {
+ private:
+ // Request
+ OrthancPluginHttpMethod method_;
+ std::string path_;
+ HttpHeaders requestHeaders_;
+ std::string requestBody_;
+ bool afterPlugins_;
+
+ // Answer
+ uint16_t httpStatus_;
+ HttpHeaders answerHeaders_;
+ std::string answerBody_;
+
+ public:
+ RestApiClient();
+
+ void SetMethod(OrthancPluginHttpMethod method)
+ {
+ method_ = method;
+ }
+
+ OrthancPluginHttpMethod GetMethod() const
+ {
+ return method_;
+ }
+
+ void SetPath(const std::string& path)
+ {
+ path_ = path;
+ }
+
+ const std::string& GetPath() const
+ {
+ return path_;
+ }
+
+ void AddRequestHeader(const std::string& key,
+ const std::string& value);
+
+ const HttpHeaders& GetRequestHeaders() const
+ {
+ return requestHeaders_;
+ }
+
+ void SetRequestBody(const std::string& body)
+ {
+ requestBody_ = body;
+ }
+
+ void SwapRequestBody(std::string& body)
+ {
+ requestBody_.swap(body);
+ }
+
+ void SetAfterPlugins(bool afterPlugins)
+ {
+ afterPlugins_ = afterPlugins;
+ }
+
+ bool IsAfterPlugins() const
+ {
+ return afterPlugins_;
+ }
+
+ const std::string& GetRequestBody() const
+ {
+ return requestBody_;
+ }
+
+ bool Execute();
+
+ uint16_t GetHttpStatus() const;
+
+ bool LookupAnswerHeader(std::string& value,
+ const std::string& key) const;
+
+ const std::string& GetAnswerBody() const;
+ };
+#endif
}
=====================================
TODO
=====================================
@@ -10,6 +10,8 @@
curl -H "Accept: multipart/related; type=application/octet-stream" http://localhost:8043/dicom-web/studies/1.2.156.112536.1.2143.25015081191207.14610300430.5/series/1.2.156.112536.1.2143.25015081191207.14610300430.6/instances/1.2.156.112536.1.2143.25015081191207.14610309990.44/frames/3 --output /tmp/out.bin
check for these logs: DICOMweb RetrieveFrames: Transcoding instance a7aec17a-e296e51f-2abe8ad8-bc95d57b-4de269d0 to transfer syntax 1.2.840.10008.1.2.1
+ Note: This has been partially handled in 1.18: Tf no transcoding is needed, we avoid downloading the full instance from Orthanc
+
We should very likely implement a cache in the DicomWEB plugin and make sure that, if 3 clients are requesting the same instance at the same time, we only
request one transcoding.
@@ -18,6 +20,7 @@
https://discourse.orthanc-server.org/t/possible-memory-leak-with-multiframe-dicom-orthanc-ohif/3988/12
+
* Implement capabilities: https://www.dicomstandard.org/using/dicomweb/capabilities/
from https://groups.google.com/d/msgid/orthanc-users/c60227f2-c6da-4fd9-9b03-3ce9bf7d1af5n%40googlegroups.com?utm_medium=email&utm_source=footer
=====================================
WebApplication/app.js
=====================================
@@ -35,8 +35,6 @@ var app = new Vue({
showStudies: false,
showSeries: false,
maxResults: MAX_RESULTS,
- currentPage: 0,
- perPage: 10,
servers: [ ],
serversInfo: { },
activeServer: '',
@@ -49,81 +47,37 @@ var app = new Vue({
jobDetails: '',
studiesFields: [
{
- key: DICOM_TAG_PATIENT_ID + '.Value',
- label: 'Patient ID',
- sortable: true
+ key: DICOM_TAG_PATIENT_ID,
+ label: 'Patient ID'
},
{
- key: DICOM_TAG_PATIENT_NAME + '.Value',
- label: 'Patient name',
- sortable: true
+ key: DICOM_TAG_PATIENT_NAME,
+ label: 'Patient name'
},
{
- key: DICOM_TAG_ACCESSION_NUMBER + '.Value',
- label: 'Accession number',
- sortable: true
+ key: DICOM_TAG_ACCESSION_NUMBER,
+ label: 'Accession number'
},
{
- key: DICOM_TAG_STUDY_DATE + '.Value',
- label: 'Study date',
- sortable: true
- },
- {
- key: 'operations',
- label: ''
+ key: DICOM_TAG_STUDY_DATE,
+ label: 'Study date'
}
],
studyToDelete: null,
studyTags: [ ],
- studyTagsFields: [
- {
- key: 'Tag',
- sortable: true
- },
- {
- key: 'Name',
- label: 'Description',
- sortable: true
- },
- {
- key: 'Value',
- sortable: true
- }
- ],
series: [ ],
seriesFields: [
{
- key: DICOM_TAG_SERIES_DESCRIPTION + '.Value',
- label: 'Series description',
- sortable: true
- },
- {
- key: DICOM_TAG_MODALITY + '.Value',
- label: 'Modality',
- sortable: true
+ key: DICOM_TAG_SERIES_DESCRIPTION,
+ label: 'Series description'
},
{
- key: 'operations',
- label: ''
+ key: DICOM_TAG_MODALITY,
+ label: 'Modality'
}
],
seriesToDelete: null,
- seriesTags: [ ],
- seriesTagsFields: [
- {
- key: 'Tag',
- sortable: true
- },
- {
- key: 'Name',
- label: 'Description',
- sortable: true
- },
- {
- key: 'Value',
- sortable: true
- }
- ],
+ seriesTags: [ ],
scrollToSeries: false,
scrollToStudies: false
},
@@ -155,7 +109,7 @@ var app = new Vue({
window.scrollTo(0, element.offsetTop);
},
ShowErrorModal: function() {
- app.$refs['modal-error'].show();
+ bootstrap.Modal.getOrCreateInstance('#modal-error').show();
},
RefreshJobDetails: function() {
axios
@@ -247,8 +201,8 @@ var app = new Vue({
item['Tag'] = i;
return item;
});
-
- app.$refs['study-details'].show();
+
+ bootstrap.Modal.getOrCreateInstance('#study-details').show();
},
RetrieveStudy: function(study) {
var base = '../../servers/';
@@ -260,13 +214,13 @@ var app = new Vue({
app.jobLevel = 'study';
app.jobId = response.data.ID;
app.jobUri = base + response.data.Path;
- app.$refs['retrieve-job'].show();
+ bootstrap.Modal.getOrCreateInstance('#retrieve-job').show();
app.RefreshJobDetails();
});
},
ConfirmDeleteStudy: function(study) {
app.studyToDelete = study;
- app.$bvModal.show('study-delete-confirm');
+ bootstrap.Modal.getOrCreateInstance('#study-delete-confirm').show();
},
ExecuteDeleteStudy: function(study) {
axios
@@ -312,7 +266,7 @@ var app = new Vue({
return item;
});
- app.$refs['series-details'].show();
+ bootstrap.Modal.getOrCreateInstance('#series-details').show();
},
RetrieveSeries: function(series) {
var base = '../../servers/';
@@ -325,7 +279,7 @@ var app = new Vue({
app.jobLevel = 'series';
app.jobId = response.data.ID;
app.jobUri = base + response.data.Path;
- app.$refs['retrieve-job'].show();
+ bootstrap.Modal.getOrCreateInstance('#retrieve-job').show();
app.RefreshJobDetails();
});
},
@@ -359,13 +313,13 @@ var app = new Vue({
app.previewFailure = true;
})
.finally(function() {
- app.$refs['series-preview'].show();
+ bootstrap.Modal.getOrCreateInstance('#series-preview').show();
})
})
},
ConfirmDeleteSeries: function(series) {
app.seriesToDelete = series;
- app.$bvModal.show('series-delete-confirm');
+ bootstrap.Modal.getOrCreateInstance('#series-delete-confirm').show();
},
ExecuteDeleteSeries: function(series) {
axios
=====================================
WebApplication/index.html
=====================================
@@ -6,9 +6,7 @@
<title>Orthanc - DICOMweb client</title>
- <!-- Add Bootstrap and Bootstrap-Vue CSS to the <head> section -->
<link type="text/css" rel="stylesheet" href="../libs/css/bootstrap.min.css"/>
- <link type="text/css" rel="stylesheet" href="../libs/css/bootstrap-vue.min.css"/>
<link type="text/css" rel="stylesheet" href="../libs/css/font-awesome.min.css"/>
<script src="../libs/js/polyfill.min.js"></script>
@@ -16,6 +14,10 @@
<!-- CSS style to truncate long text in tables, provided they have
class "table-layout:fixed;" or attribute ":fixed=true" -->
<style>
+ table {
+ table-layout:fixed;
+ }
+
table td {
white-space: nowrap;
overflow: hidden;
@@ -28,7 +30,7 @@
<div class="container" id="app">
<p style="height:1em"></p>
- <div class="jumbotron">
+ <div class="bg-light mb-4 rounded-2 py-5 px-3">
<div class="row">
<div class="col-sm-8">
<h1 class="display-4">DICOMweb client</h1>
@@ -60,47 +62,70 @@
</div>
- <b-modal ref="modal-error" size="xl" ok-only="true">
- <template slot="modal-title">
- Connection error
- </template>
- <div class="d-block">
- <p>
- There was an error connecting to "{{ activeServer }}" server.
- </p>
+ <div class="modal" tabindex="-1" id="modal-error">
+ <div class="modal-dialog modal-xl">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Connection error</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p>
+ There was an error connecting to "{{ activeServer }}" server.
+ </p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
+ </div>
+ </div>
</div>
- </b-modal>
+ </div>
+
-
<!-- LOOKUP -->
<div class="row">
- <b-alert variant="danger" dismissible v-model="showNoServer">
+ <div class="alert alert-danger alert-dismissible" v-if="showNoServer">
No DICOMweb server is configured!
- </b-alert>
- <b-form style="width:100%;padding:5px;">
- <b-form-group label="DICOMweb server:" label-cols-sm="4" label-cols-lg="3">
- <b-form-select v-model="lookup.server" :options="servers"></b-form-select>
- </b-form-group>
- <b-form-group label="Patient ID:" label-cols-sm="4" label-cols-lg="3">
- <b-form-input v-model="lookup.patientID"></b-form-input>
- </b-form-group>
- <b-form-group label="Patient name:" label-cols-sm="4" label-cols-lg="3">
- <b-form-input v-model="lookup.patientName"></b-form-input>
- </b-form-group>
- <b-form-group label="Accession number:" label-cols-sm="4" label-cols-lg="3">
- <b-form-input v-model="lookup.accessionNumber"></b-form-input>
- </b-form-group>
- <b-form-group label="Study date:" label-cols-sm="4" label-cols-lg="3">
- <b-form-input v-model="lookup.studyDate"></b-form-input>
- </b-form-group>
+ </div>
+ <form style="width:100%;padding:5px;">
+ <div class="mb-3 row">
+ <label class="col-form-label col-sm-3 cols-lg-3">DICOMweb server:</label>
+ <div class="col-sm-9 cols-lg-9">
+ <select class="form-select" v-model="lookup.server">
+ <option v-for="option in servers">{{ option }}</option>
+ </select>
+ </div>
+ </div>
+ <div class="mb-3 row">
+ <label class="col-form-label col-sm-3 cols-lg-3">Patient ID:</label>
+ <div class="col-sm-9 cols-lg-9">
+ <input type="text" class="form-control" v-model="lookup.patientID"></input>
+ </div>
+ </div>
+ <div class="mb-3 row">
+ <label class="col-form-label col-sm-3 cols-lg-3">Patient name:</label>
+ <div class="col-sm-9 cols-lg-9">
+ <input type="text" class="form-control" v-model="lookup.patientName"></input>
+ </div>
+ </div>
+ <div class="mb-3 row">
+ <label class="col-form-label col-sm-3 cols-lg-3">Accession number:</label>
+ <div class="col-sm-9 cols-lg-9">
+ <input type="text" class="form-control" v-model="lookup.accessionNumber"></input>
+ </div>
+ </div>
+ <div class="mb-3 row">
+ <label class="col-form-label col-sm-3 cols-lg-3">Study date:</label>
+ <div class="col-sm-9 cols-lg-9">
+ <input type="text" class="form-control" v-model="lookup.studyDate"></input>
+ </div>
+ </div>
<p class="pull-right">
- <b-button type="submit" variant="success" @click="OnLookup"
- size="lg">Do lookup</b-button>
- <b-button type="reset" variant="outline-danger" @click="OnReset"
- size="lg">Reset</b-button>
+ <button type="button" class="btn btn-lg btn-success" @click="OnLookup">Do lookup</button>
+ <button type="button" class="btn btn-lg btn-outline-danger" @click="OnReset">Reset</button>
</p>
- </b-form>
+ </form>
</div>
@@ -111,54 +136,96 @@
<h1>Studies</h1>
</div>
<div class="row" v-show="showStudies">
- <b-alert variant="warning" dismissible v-model="showTruncatedStudies">
+ <div class="alert alert-warning alert-dismissible" v-if="showTruncatedStudies">
More than {{ maxResults }} matching studies, results have been truncated!
- </b-alert>
+ </div>
</div>
<div class="row" v-show="showStudies">
- <b-pagination v-model="currentPage" :per-page="perPage" :total-rows="studiesCount"></b-pagination>
- <b-table striped hover :current-page="currentPage" :per-page="perPage"
- :items="studies" :fields="studiesFields" :fixed="false">
- <template slot="operations" slot-scope="data">
- <b-button @click="OpenSeries(data.item)" title="Open series">
- <i class="fa fa-folder-open"></i>
- </b-button>
- <b-button @click="OpenStudyDetails(data.item)" title="Open tags">
- <i class="fa fa-address-card"></i>
- </b-button>
- <b-button @click="RetrieveStudy(data.item)" title="Retrieve study using WADO-RS">
- <i class="fa fa-cloud-download"></i>
- </b-button>
- <b-button @click="ConfirmDeleteStudy(data.item)"
- v-if="serversInfo[activeServer].HasDelete == '1'" title="Delete remote study">
- <i class="fa fa-trash"></i>
- </b-button>
- </template>
- </b-table>
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th scope="col" v-for="column in studiesFields">{{column.label}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="s in studies">
+ <td v-for="column in studiesFields">{{column.key in s ? s[column.key].Value : ''}}</td>
+ <td>
+ <button type="button" class="btn btn-secondary" @click="OpenSeries(s)" title="Open series">
+ <i class="fa fa-folder-open"></i>
+ </button>
+ <button type="button" class="btn btn-secondary" @click="OpenStudyDetails(s)" title="Open tags">
+ <i class="fa fa-address-card"></i>
+ </button>
+ <button type="button" class="btn btn-secondary" @click="RetrieveStudy(s)" title="Retrieve study using WADO-RS">
+ <i class="fa fa-cloud-download"></i>
+ </button>
+ <button type="button" class="btn btn-secondary" @click="ConfirmDeleteStudy(s)" title="Delete remote study"
+ v-if="serversInfo[activeServer].HasDelete == '1'">
+ <i class="fa fa-trash"></i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
- <b-modal ref="study-details" size="xl" ok-only="true">
- <template slot="modal-title">
- Details of study
- </template>
- <div class="d-block text-center">
- <b-table striped :items="studyTags" :fields="studyTagsFields" :fixed="true">
- </b-table>
+
+ <div class="modal" tabindex="-1" id="study-details">
+ <div class="modal-dialog modal-xl">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Details of study</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th scope="col" class="col-sm-2">Tag</th>
+ <th scope="col" class="col-sm-3">Description</th>
+ <th scope="col" class="col-sm-7">Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="s in studyTags">
+ <td class="col-sm-2">{{'Tag' in s ? s.Tag : ''}}</td>
+ <td class="col-sm-3">{{'Name' in s ? s.Name : ''}}</td>
+ <td class="col-sm-7">{{'Value' in s ? s.Value : ''}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
+ </div>
</div>
- </b-modal>
+ </div>
+ </div>
- <b-modal id="study-delete-confirm" size="xl" @ok="ExecuteDeleteStudy">
- <template slot="modal-title">
- Confirm deletion
- </template>
- <div class="d-block">
- <p>
- Are you sure you want to remove this study from the remote server?
- </p>
- <p>
- Patient name: {{ studyToDelete && studyToDelete['00100010'] && studyToDelete['00100010'].Value }}
- </p>
+
+ <div class="modal" tabindex="-1" id="study-delete-confirm">
+ <div class="modal-dialog modal-xl">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Confirm deletion</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p>
+ Are you sure you want to remove this study from the remote server?
+ </p>
+ <p>
+ Patient name: {{ studyToDelete && studyToDelete['00100010'] && studyToDelete['00100010'].Value }}
+ </p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal" @click="ExecuteDeleteStudy">Confirm</button>
+ </div>
</div>
- </b-modal>
+ </div>
</div>
@@ -169,92 +236,154 @@
<h1>Series</h1>
</div>
<div class="row" v-show="showSeries">
- <b-table striped hover :items="series" :fields="seriesFields" :fixed="false">
- <template slot="operations" slot-scope="data">
- <b-button @click="OpenSeriesPreview(data.item)" title="Preview">
- <i class="fa fa-eye"></i>
- </b-button>
- <b-button @click="OpenSeriesDetails(data.item)" title="Open tags">
- <i class="fa fa-address-card"></i>
- </b-button>
- <b-button @click="RetrieveSeries(data.item)" title="Retrieve series using WADO-RS">
- <i class="fa fa-cloud-download"></i>
- </b-button>
- <b-button @click="ConfirmDeleteSeries(data.item)"
- v-if="serversInfo[activeServer].HasDelete" title="Delete remote series">
- <i class="fa fa-trash"></i>
- </b-button>
- </template>
- </b-table>
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th scope="col" v-for="column in seriesFields">{{column.label}}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="s in series">
+ <td v-for="column in seriesFields">{{column.key in s ? s[column.key].Value : ''}}</td>
+ <td>
+ <button type="button" class="btn btn-secondary" @click="OpenSeriesPreview(s)" title="Preview">
+ <i class="fa fa-eye"></i>
+ </button>
+ <button type="button" class="btn btn-secondary" @click="OpenSeriesDetails(s)" title="Open tags">
+ <i class="fa fa-address-card"></i>
+ </button>
+ <button type="button" class="btn btn-secondary" @click="RetrieveSeries(s)" title="Retrieve series using WADO-RS">
+ <i class="fa fa-cloud-download"></i>
+ </button>
+ <button type="button" class="btn btn-secondary" @click="ConfirmDeleteSeries(s)" title="Delete remote series"
+ v-if="serversInfo[activeServer].HasDelete == '1'">
+ <i class="fa fa-trash"></i>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
- <b-modal ref="series-details" size="xl" ok-only="true">
- <template slot="modal-title">
- Details of series
- </template>
- <div class="d-block text-center">
- <b-table striped :items="seriesTags" :fields="seriesTagsFields" :fixed="true">
- </b-table>
+
+ <div class="modal" tabindex="-1" id="series-preview">
+ <div class="modal-dialog modal-xl">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Preview of series</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <div class="alert alert-danger alert-dismissible" v-if="previewFailure">
+ The remote DICOMweb server cannot generate a preview for this image.
+ </div>
+ <div class="text-center">
+ <img class="img-fluid" v-if="!previewFailure" v-bind:src="preview">
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
+ </div>
</div>
- </b-modal>
+ </div>
+ </div>
- <b-modal ref="series-preview" size="xl" ok-only="true">
- <template slot="modal-title">
- Preview of series
- </template>
- <div class="d-block text-center">
- <b-alert variant="danger" v-model="previewFailure">
- The remote DICOMweb server cannot generate a preview for this image.
- </b-alert>
- <b-img v-if="!previewFailure" :src="preview" fluid alt=""></b-img>
+
+ <div class="modal" tabindex="-1" id="series-details">
+ <div class="modal-dialog modal-xl">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Details of series</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th scope="col" class="col-sm-2">Tag</th>
+ <th scope="col" class="col-sm-3">Description</th>
+ <th scope="col" class="col-sm-7">Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="s in seriesTags">
+ <td class="col-sm-2">{{'Tag' in s ? s.Tag : ''}}</td>
+ <td class="col-sm-3">{{'Name' in s ? s.Name : ''}}</td>
+ <td class="col-sm-7">{{'Value' in s ? s.Value : ''}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
+ </div>
</div>
- </b-modal>
+ </div>
+ </div>
- <b-modal id="series-delete-confirm" size="xl" @ok="ExecuteDeleteSeries">
- <template slot="modal-title">
- Confirm deletion
- </template>
- <div class="d-block">
- <p>
- Are you sure you want to remove this series from the remote server?
- </p>
- <p>
- Series description: {{ seriesToDelete && seriesToDelete['0008103E'] && seriesToDelete['0008103E'].Value }}
- </p>
+
+ <div class="modal" tabindex="-1" id="series-delete-confirm">
+ <div class="modal-dialog modal-xl">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Confirm deletion</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p>
+ Are you sure you want to remove this series from the remote server?
+ </p>
+ <p>
+ Series description: {{ seriesToDelete && seriesToDelete['0008103E'] && seriesToDelete['0008103E'].Value }}
+ </p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal" @click="ExecuteDeleteSeries">Confirm</button>
+ </div>
</div>
- </b-modal>
+ </div>
</div>
- <b-modal ref="retrieve-job" size="xl" ok-only="true">
- <template slot="modal-title">
- Retrieving {{ jobLevel }}
- </template>
- <div class="d-block">
- <p>
- Orthanc is now running a background job to retrieve the
- {{ jobLevel }} from remote server "{{ activeServer }}" using
- WADO-RS.
- </p>
- <p>
- Job ID: <tt>{{ jobId }}</tt>
- </p>
- <p>
- Job details:
- </p>
- <pre>{{ jobDetails }}</pre>
- <p>
- <b-button variant="success" @click="RefreshJobDetails()">Refresh job details</b-button>
- </p>
+ <div class="modal" tabindex="-1" id="retrieve-job">
+ <div class="modal-dialog modal-xl">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Retrieving {{ jobLevel }}</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p>
+ Orthanc is now running a background job to retrieve the
+ {{ jobLevel }} from remote server "{{ activeServer }}" using
+ WADO-RS.
+ </p>
+ <p>
+ Job ID: <tt>{{ jobId }}</tt>
+ </p>
+ <p>
+ Job details:
+ </p>
+ <pre>{{ jobDetails }}</pre>
+ <p>
+ <button type="button" class="btn btn-success" @click="RefreshJobDetails()">Refresh job details</button>
+ </p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
+ </div>
+ </div>
</div>
- </b-modal>
+ </div>
<p style="height:5em"></p>
</div>
- <!-- Add Vue and Bootstrap-Vue JS just before the closing </body> tag -->
+ <script src="../libs/js/bootstrap.min.js"></script>
<script src="../libs/js/vue.min.js"></script>
- <script src="../libs/js/bootstrap-vue.min.js"></script>
<script src="../libs/js/axios.min.js"></script>
<script type="text/javascript" src="app.js"></script>
</body>
View it on GitLab: https://salsa.debian.org/med-team/orthanc-dicomweb/-/commit/bee0a78e91ca2f0735b1a5195e28db4eefbe0987
--
View it on GitLab: https://salsa.debian.org/med-team/orthanc-dicomweb/-/commit/bee0a78e91ca2f0735b1a5195e28db4eefbe0987
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20241220/eb4cde50/attachment-0001.htm>
More information about the debian-med-commit
mailing list