Mercurial > hg > orthanc
changeset 6166:dbe67b2c2d9c attach-custom-data
integration mainline->attach-custom-data
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Wed, 11 Jun 2025 16:01:06 +0200 |
| parents | 4cf3bfb29474 (current diff) ecd7fdc5f8d4 (diff) |
| children | 86a076ceaf3a |
| files | NEWS OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerEnumerations.h TODO |
| diffstat | 15 files changed, 261 insertions(+), 39 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Mon Jun 09 20:59:35 2025 +0200 +++ b/NEWS Wed Jun 11 16:01:06 2025 +0200 @@ -23,6 +23,24 @@ The "LimitMainDicomTagsReconstructLevel" configuration is now ignored when a full processing is required. * Fix computation of MD5 hashes for memory buffers whose size is larger than 2^31 bytes. +* Delayed Deletion plugin: + - Added an index in the delayed-deletion SQLite external DB to speed up delayed deletions. + This new index will only apply to new databases. If you wish to speed up an existing installation, + run "CREATE INDEX PendingIndex ON Pending(uuid)" manually in the plugin SQLite DB. + Patch provided by Yurii (George) from ivtech.dev. + With this patch, we observed a 100 fold performance improvement when the + "Pending" table contains 1-2 millions files. +* Configuration options "RejectSopClasses" and "RejectedSopClasses" are taken as synonyms. + In Orthanc 1.12.6 and 1.12.7, "RejectSopClasses" was used instead of the expected + "RejectedSopClasses" spelling. +* Fix the re-encoding of DICOM files larger than 4GB + +REST API +-------- + +* If the index database provides the "HasExtendedFind" primitive, the "ResponseContent" option in + "/tools/find" now allows to specify "IsProtected" to retrieve the "IsProtected" status of a + patient resource. Version 1.12.7 (2025-04-07)
--- a/OrthancFramework/Sources/ChunkedBuffer.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancFramework/Sources/ChunkedBuffer.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -25,6 +25,8 @@ #include "PrecompiledHeaders.h" #include "ChunkedBuffer.h" +#include "OrthancException.h" + #include <cassert> #include <string.h> @@ -54,7 +56,16 @@ else { assert(chunkData != NULL); - chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize)); + + try + { + chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize)); + } + catch (...) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + numBytes_ += chunkSize; } } @@ -172,24 +183,59 @@ void ChunkedBuffer::Flatten(std::string& result) { FlushPendingBuffer(); - result.resize(numBytes_); - size_t pos = 0; - for (Chunks::iterator it = chunks_.begin(); - it != chunks_.end(); ++it) + if (chunks_.empty()) { - assert(*it != NULL); - - size_t s = (*it)->size(); - if (s != 0) + if (numBytes_ != 0) { - memcpy(&result[pos], (*it)->c_str(), s); - pos += s; + throw OrthancException(ErrorCode_InternalError); } - delete *it; + result.clear(); + } + else if (chunks_.size() == 1) + { + // Avoid reallocating a buffer if there is a single chunk + assert(chunks_.front() != NULL); + if (chunks_.front()->size() != numBytes_) + { + throw OrthancException(ErrorCode_InternalError); + } + else + { + chunks_.front()->swap(result); + delete chunks_.front(); + } + } + else + { + try + { + result.resize(numBytes_); + } + catch (...) + { + throw OrthancException(ErrorCode_NotEnoughMemory); + } + + size_t pos = 0; + for (Chunks::iterator it = chunks_.begin(); + it != chunks_.end(); ++it) + { + assert(*it != NULL); + + size_t s = (*it)->size(); + if (s != 0) + { + memcpy(&result[pos], (*it)->c_str(), s); + pos += s; + } + + delete *it; + } } + // Reset the data structure chunks_.clear(); numBytes_ = 0; }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -31,7 +31,6 @@ #include "../Compatibility.h" #include "../Endianness.h" -#include "../Logging.h" #include "../OrthancException.h" #include "../Toolbox.h" #include "DicomArray.h" @@ -1220,7 +1219,7 @@ } - void DicomMap::LogMissingTagsForStore() const + std::string DicomMap::FormatMissingTagsForStore() const { std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; @@ -1244,14 +1243,14 @@ sopInstanceUid = ValueAsString(*this, DICOM_TAG_SOP_INSTANCE_UID); } - LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + return FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); } - void DicomMap::LogMissingTagsForStore(const std::string& patientId, - const std::string& studyInstanceUid, - const std::string& seriesInstanceUid, - const std::string& sopInstanceUid) + std::string DicomMap::FormatMissingTagsForStore(const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid) { std::string s, t; @@ -1309,11 +1308,11 @@ if (t.size() == 0) { - LOG(ERROR) << "Store has failed because all the required tags (" << s << ") are missing (is it a DICOMDIR file?)"; + return "Store has failed because all the required tags (" + s + ") are missing (is it a DICOMDIR file?)"; } else { - LOG(ERROR) << "Store has failed because required tags (" << s << ") are missing for the following instance: " << t; + return "Store has failed because required tags (" + s + ") are missing for the following instance: " + t; } }
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h Wed Jun 11 16:01:06 2025 +0200 @@ -171,12 +171,12 @@ const void* dicom, size_t size); - void LogMissingTagsForStore() const; + std::string FormatMissingTagsForStore() const; - static void LogMissingTagsForStore(const std::string& patientId, - const std::string& studyInstanceUid, - const std::string& seriesInstanceUid, - const std::string& sopInstanceUid); + static std::string FormatMissingTagsForStore(const std::string& patientId, + const std::string& studyInstanceUid, + const std::string& seriesInstanceUid, + const std::string& sopInstanceUid); bool LookupStringValue(std::string& result, const DicomTag& tag,
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -192,7 +192,7 @@ if (e.GetErrorCode() == ErrorCode_InexistentTag) { - FromDcmtkBridge::LogMissingTagsForStore(**imageDataSet); + LOG(ERROR) << FromDcmtkBridge::FormatMissingTagsForStore(**imageDataSet); } else {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -38,6 +38,7 @@ #include "FromDcmtkBridge.h" #include "ToDcmtkBridge.h" +#include "../ChunkedBuffer.h" #include "../Compatibility.h" #include "../Logging.h" #include "../Toolbox.h" @@ -167,6 +168,73 @@ namespace { + class ChunkedBufferStream : public DcmOutputStream + { + private: + class Consumer : public DcmConsumer + { + private: + ChunkedBuffer buffer_; + + public: + void Flatten(std::string& buffer) + { + buffer_.Flatten(buffer); + } + + OFBool good() const ORTHANC_OVERRIDE + { + return true; + } + + OFCondition status() const ORTHANC_OVERRIDE + { + return EC_Normal; + } + + OFBool isFlushed() const ORTHANC_OVERRIDE + { + return true; + } + + offile_off_t avail() const ORTHANC_OVERRIDE + { + // since we cannot report "unlimited", let's claim that we can still write 10MB. + // Note that offile_off_t is a signed type. + return 10 * 1024 * 1024; + } + + offile_off_t write(const void *buf, + offile_off_t buflen) ORTHANC_OVERRIDE + { + buffer_.AddChunk(buf, buflen); + return buflen; + } + + void flush() ORTHANC_OVERRIDE + { + // Nothing to flush + } + }; + + Consumer consumer_; + + public: + ChunkedBufferStream() : + DcmOutputStream(&consumer_) + { + } + + void Flatten(std::string& buffer) + { + consumer_.Flatten(buffer); + } + }; + } + + + namespace + { class DictionaryLocker : public boost::noncopyable { private: @@ -1572,7 +1640,12 @@ } - +#if 0 + /** + * This was the implementation in Orthanc <= 1.12.7. This version + * uses "DcmFileFormat::calcElementLength()", which cannot handle + * DICOM files whose size cannot be represented on 32 bits. + **/ static bool SaveToMemoryBufferInternal(std::string& buffer, DcmFileFormat& dicom, E_TransferSyntax xfer, @@ -1619,6 +1692,46 @@ return false; } } +#endif + + +#if 1 + /** + * This is the cleaner implementation used in Orthanc >= 1.12.8, + * which allows to write DICOM files larger than 4GB. + **/ + static bool SaveToMemoryBufferInternal(std::string& buffer, + DcmFileFormat& dicom, + E_TransferSyntax xfer, + std::string& errorMessage) + { + ChunkedBufferStream ob; + + // Fill the (chunked) memory buffer with the meta-header and the dataset + dicom.transferInit(); + OFCondition c = dicom.write(ob, xfer, /*opt_sequenceType*/ EET_ExplicitLength, NULL, + /*opt_groupLength*/ EGL_recalcGL, + /*opt_paddingType*/ EPD_noChange, + /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0, + EWM_updateMeta /* creates new SOP instance UID on lossy */); + dicom.transferEnd(); + + if (c.good()) + { + ob.flush(); + ob.Flatten(buffer); + return true; + } + else + { + // Error + buffer.clear(); + errorMessage = std::string(c.text()); + return false; + } + } +#endif + bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer, DcmDataset& dataSet) @@ -3162,7 +3275,7 @@ } - void FromDcmtkBridge::LogMissingTagsForStore(DcmDataset& dicom) + std::string FromDcmtkBridge::FormatMissingTagsForStore(DcmDataset& dicom) { std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; @@ -3194,7 +3307,7 @@ sopInstanceUid.assign(c); } - DicomMap::LogMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + return DicomMap::FormatMissingTagsForStore(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); }
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h Wed Jun 11 16:01:06 2025 +0200 @@ -280,7 +280,7 @@ static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target, DcmDataset& dicom); - static void LogMissingTagsForStore(DcmDataset& dicom); + static std::string FormatMissingTagsForStore(DcmDataset& dicom); static void RemovePath(DcmDataset& dataset, const DicomPath& path);
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/PendingDeletionsDatabase.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -42,6 +42,12 @@ if (!db_.DoesTableExist("Pending")) { db_.Execute("CREATE TABLE Pending(uuid TEXT, type INTEGER)"); + + // New in v 1.12.7+ + // add an index on uuid to speed up the DELETE FROM Pending WHERE uuid=? + // With this patch, we observed a 100 fold performance + // improvement when the Pending table contains 1-2 millions files. + db_.Execute("CREATE INDEX PendingIndex ON Pending(uuid)"); } t.Commit();
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -705,8 +705,8 @@ { call.GetDocumentation().SetHttpGetArgument(GET_RESPONSE_CONTENT, RestApiCallDocumentation::Type_String, "Defines the content of response for each returned resource. Allowed values are `MainDicomTags`, " - "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. If not specified, Orthanc " - "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`." + "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`, `Attachments`. If not specified, Orthanc " + "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`." "e.g: '" + GET_RESPONSE_CONTENT + "=MainDicomTags;Children " "(new in Orthanc 1.12.5 - overrides `expand`)", false); @@ -719,8 +719,8 @@ call.GetDocumentation().SetRequestField(POST_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings, "Defines the content of response for each returned resource. (this field, if present, overrides the \"Expand\" field). " "Allowed values are `MainDicomTags`, " - "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`. If not specified, Orthanc " - "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`." + "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`, `Attachments`. If not specified, Orthanc " + "will return `MainDicomTags`, `Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `IsProtected`." "(new in Orthanc 1.12.5)", false); call.GetDocumentation().SetRequestField(POST_EXPAND, RestApiCallDocumentation::Type_Boolean,
--- a/OrthancServer/Sources/ResourceFinder.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancServer/Sources/ResourceFinder.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -394,6 +394,14 @@ } } + if (responseContent_ & ResponseContentFlags_IsProtected) + { + if (resource.GetLevel() == ResourceType_Patient ) + { + target["IsProtected"] = index.IsProtectedPatient(resource.GetIdentifier()); + } + } + if (responseContent_ & ResponseContentFlags_MainDicomTags) { DicomMap allMainDicomTags;
--- a/OrthancServer/Sources/ResourceFinder.h Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancServer/Sources/ResourceFinder.h Wed Jun 11 16:01:06 2025 +0200 @@ -178,7 +178,7 @@ request_.SetRetrieveAttachments(retrieve); } - // NB: "index" is only used in this method to fill the "IsStable" information + // NB: "index" is used in this method to fill the "IsStable" and "IsProtected" information void Expand(Json::Value& target, const FindResponse::Resource& resource, ServerIndex& index,
--- a/OrthancServer/Sources/ServerContext.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -495,7 +495,27 @@ std::list<std::string> acceptedSopClasses; std::set<std::string> rejectedSopClasses; lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses"); - lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses"); + + static const char* const REJECTED_SOP_CLASS = "RejectedSopClasses"; + if (lock.GetJson().isMember(REJECTED_SOP_CLASS)) + { + lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, REJECTED_SOP_CLASS); + } + else + { + static const char* const REJECT_SOP_CLASS = "RejectSopClasses"; + if (lock.GetJson().isMember(REJECT_SOP_CLASS)) + { + /** + * This is for backward compatibility. In Orthanc 1.12.6, + * there was a typo: "RejectedSopClasses" was spelled as + * "RejectSopClasses". + * https://discourse.orthanc-server.org/t/fix-for-config-param-rejectsopclasses-vs-rejectedsopclasses/5900 + **/ + lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, REJECT_SOP_CLASS); + } + } + SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses); defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE")); @@ -874,7 +894,7 @@ { if (e.GetErrorCode() == ErrorCode_InexistentTag) { - summary.LogMissingTagsForStore(); + LOG(ERROR) << summary.FormatMissingTagsForStore(); } throw;
--- a/OrthancServer/Sources/ServerEnumerations.cpp Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.cpp Wed Jun 11 16:01:06 2025 +0200 @@ -655,6 +655,10 @@ { return ResponseContentFlags_IsStable; } + else if (value == "IsProtected") + { + return ResponseContentFlags_IsProtected; + } else { throw OrthancException(ErrorCode_ParameterOutOfRange,
--- a/OrthancServer/Sources/ServerEnumerations.h Mon Jun 09 20:59:35 2025 +0200 +++ b/OrthancServer/Sources/ServerEnumerations.h Wed Jun 11 16:01:06 2025 +0200 @@ -136,6 +136,7 @@ ResponseContentFlags_Children = (1 << 10), ResponseContentFlags_Labels = (1 << 11), ResponseContentFlags_IsStable = (1 << 12), + ResponseContentFlags_IsProtected = (1 << 13), ResponseContentFlags_INTERNAL_CountResources = (1 << 30), @@ -150,7 +151,8 @@ ResponseContentFlags_Parent | ResponseContentFlags_Children | ResponseContentFlags_Labels | - ResponseContentFlags_IsStable), // equivalent to "Expand": true + ResponseContentFlags_IsStable | + ResponseContentFlags_IsProtected), // equivalent to "Expand": true ResponseContentFlags_Default = (ResponseContentFlags_ID | ResponseContentFlags_Type |
--- a/TODO Mon Jun 09 20:59:35 2025 +0200 +++ b/TODO Wed Jun 11 16:01:06 2025 +0200 @@ -66,6 +66,12 @@ * Allow saving PrivateTags in ExtraMainDicomTags. Note: they can actually be stored but they then appear as "Unknown Tag & Data" in the responses. If we try to add the PrivateCreator in the ExtraMainDicomTags, then, the DICOMWeb plugin fails to initialize because the private tags are not known. +* Support hashed passwords in RegisteredUsers. E.g: + "RegisteredUsers": { + "admin": { + "Password": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8", + "Hashing": "sha1"} } + ============================ Documentation (Orthanc Book)
