Mercurial > hg > orthanc
changeset 6399:1facef20a778 queues-timeout
integration mainline->queues-timeout
| author | Sebastien Jodogne <s.jodogne@gmail.com> |
|---|---|
| date | Wed, 12 Nov 2025 19:06:12 +0100 |
| parents | 502f8cbd533e (current diff) 55db426a8667 (diff) |
| children | 6766053ee920 |
| files | NEWS OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Sources/Database/PrepareDatabase.sql TODO |
| diffstat | 107 files changed, 1558 insertions(+), 654 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Tue Oct 28 10:29:11 2025 +0100 +++ b/NEWS Wed Nov 12 19:06:12 2025 +0100 @@ -7,7 +7,7 @@ ------- * New configuration "HttpBindAddresses" to list the IP addresses on which the - HTTP server listens. By default, this list is empty and the HTTP server listens + HTTP server listens. By default, this list is empty and the HTTP server listens on all network interfaces. REST API @@ -15,34 +15,67 @@ * Fix: C-Get SCU jobs or HTTP responses were always successful even if the C-Get operation actually failed. -* C-Store, C-Move and C-Get jobs and HTTP responses now include a new "DimseErrorStatus" +* C-Store, C-Move and C-Get jobs and HTTP responses now include a new "DimseErrorStatus" field (ONLY if the operation fails). - +* C-Move and C-Get jobs and HTTP responses now include a new "Details" field with the "DimseErrorStatus" + of each operation and the list of "RetrievedInstancesIds". Note that, if you have multiple + resources to retrieve and one of them fails, you'll only get those details if you have set + "Permissive" to true to allow other operations to continue after the failure. Maintenance ----------- -* Under Windows, the console output is now configured to interpret strings as UTF-8. - This notably allow cyrillic strings to display correctly in the console. -* Improved streaming of HTTP responses under OS pressure. In some conditions, when the OS - was not able to send a full buffer over the network, Orthanc was not retrying - to send the remaining part. This is now fixed. +* Under Microsoft Windows, the console output is now configured to interpret strings as UTF-8. + This notably allow Cyrillic strings to display correctly in the console. +* Improved streaming of HTTP responses under OS pressure. In some conditions, when the OS + was not able to send a full buffer over the network, Orthanc was not retrying + to send the remaining part. (https://discourse.orthanc-server.org/t/incomplete-zip-downloads-from-get-studies-id-media/6046) * Reworked all the paths handling, improving general support of non ASCII-only paths on Windows, specifically for the Storage directories and for the configuration files. - However, on Windows, some features might still not support non ASCII-only paths: + However, on Microsoft Windows, some features might still not support non ASCII-only paths: - Reading a DCMTK dictionary ("ExternalDictionaries" configuration) - Using a "TemporaryDirectory" to save zip file or to export DICOMDIR - The "SslCertificate" and other related configurations * Fix: DicomGetScu jobs are now saved in DB. * Fix: When the configuration option "MaximumStorageCacheSize" was set to 0, the default value (128) - was actually applied. From now on, a value of 0 really means that the storage cache is disabled. -* Fix: Orthanc was unable to convert the tags into dicom+json format if the instance contained an - empty element in a sequence. This was preventing access to /dicom-web/../metadata routes and prevented + was actually applied. From now on, a value of 0 really means that the storage cache is disabled. +* Fix: Orthanc was unable to convert the tags into dicom+json format if the instance contained an + empty element in a sequence. This was preventing access to /dicom-web/../metadata routes and visualization in e.g. the Stone Web viewer and OHIF. -* Fix issue 252: Disallow colons in HTTP basic usernames +* Fix issue #252: Disallow colons in HTTP basic usernames +* Fix: Decoding of LUT with less than 256 entries: + https://discourse.orthanc-server.org/t/cannot-preview-dicom-file/6275 +* Fix: Possible deadlocks when using ZipLoaderThreads > 1 when the HttpClient disconnects + while downloading the response. +* Fix issue #227: In SQL, string literals must use 'single quotes' + https://stackoverflow.com/a/25141338 * Upgraded dependencies for static builds: - civetweb 1.16, including patch for CVE-2025-55763 + - SQLite 3.50.4 + +Plugins +------- + +* Worklists plugin: + - The Worklists plugin now provides a REST API to: + - create worklists through POST at /worklists/create + - list the worklists through GET at /worklists + - view a single worklist through GET at /worklists/{uuid} + - modify a worklist through PUT at /worklists/{uuid} + - delete a worklist through DELETE at /worklists/{uuid} + All details are available in the Orthanc book: + https://orthanc.uclouvain.be/book/plugins/worklists-plugin.html + - New configuration options: + - "SaveInOrthancDatabase" to store the worklists in the Orthanc DB (provided that you are using SQLite or PostgreSQL). + - "DeleteWorklistsOnStableStudy" to delete the worklist once its related study has been received and is stable. + - "SetStudyInstanceUidIfMissing" to add a StudyInstanceUID if the REST API request does not include one. + - "DeleteWorklistsDelay" to delete a worklist N hours after it has been created + (only available if using the "SaveInOrthancDatabase" mode). + - "HousekeepingInterval" to define the delay between 2 executions of the Worklist Housekeeping thread + that deletes the worklists when required. + - Note: the previous "Database" configuration has now been renamed in "Directory" to better differentiate + the "File" or "DB modes. Plugin SDK ---------- @@ -379,7 +412,7 @@ * Upgraded dependencies for static builds: - boost 1.86.0 - curl 8.9.0 - - SQLite 3.46 + - SQLite 3.46.1 Version 1.12.4 (2024-06-05) @@ -656,7 +689,7 @@ * Enforce the existence of the patient/study/instance while creating its archive * Security: New configuration option "RestApiWriteToFileSystemEnabled" to allow "/instances/../export" (the latter is now disabled by default) -* Fix issue 214: VOILUTSequence is not returned in Wado-RS +* Fix issue #214: VOILUTSequence is not returned in Wado-RS * Fix /tools/reset crashing when ExtraMainDicomTags were defined * Fix Housekeeper plugin infinite loop if Orthanc is empty. * Fix a crash in /tools/reconstruct triggered by the Housekeeper plugin
--- a/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake Wed Nov 12 19:06:12 2025 +0100 @@ -28,9 +28,9 @@ if (SQLITE_STATIC) - SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3460100) - SET(SQLITE_MD5 "1fb0f7ebbee45752098cf453b6dffff3") - SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3460100.zip") + SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3500400) + SET(SQLITE_MD5 "440abd85c5ee3297dd388ade51fec0cc") + SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3500400.zip") set(ORTHANC_SQLITE_VERSION 3046001)
--- a/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h Wed Nov 12 19:06:12 2025 +0100 @@ -51,7 +51,7 @@ const void* pixelData, size_t size); - const DicomImageInformation GetInformation() const + const DicomImageInformation& GetInformation() const { return information_; }
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -156,7 +156,7 @@ } - void DicomStreamReader::HandlePreamble(IVisitor& visitor, + void DicomStreamReader::HandlePreamble(const IVisitor& visitor, const std::string& block) { assert(block.size() == 144u);
--- a/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomStreamReader.h Wed Nov 12 19:06:12 2025 +0100 @@ -89,7 +89,7 @@ bool IsLittleEndian() const; - void HandlePreamble(IVisitor& visitor, + void HandlePreamble(const IVisitor& visitor, const std::string& block); void HandleMetaHeader(IVisitor& visitor,
--- a/OrthancFramework/Sources/DicomFormat/DicomTag.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h Wed Nov 12 19:06:12 2025 +0100 @@ -192,6 +192,7 @@ static const DicomTag DICOM_TAG_FRAME_INCREMENT_POINTER(0x0028, 0x0009); static const DicomTag DICOM_TAG_GRID_FRAME_OFFSET_VECTOR(0x3004, 0x000c); static const DicomTag DICOM_TAG_PIXEL_SPACING(0x0028, 0x0030); + static const DicomTag DICOM_TAG_IMAGER_PIXEL_SPACING(0x0018, 0x1164); static const DicomTag DICOM_TAG_RESCALE_INTERCEPT(0x0028, 0x1052); static const DicomTag DICOM_TAG_RESCALE_SLOPE(0x0028, 0x1053); static const DicomTag DICOM_TAG_SLICE_THICKNESS(0x0018, 0x0050); @@ -238,6 +239,10 @@ static const DicomTag DICOM_TAG_REFERENCED_FILE_ID(0x0004, 0x1500); // Tags for DicomWeb - static const Orthanc::DicomTag DICOM_TAG_RETRIEVE_URL(0x0008, 0x1190); + static const DicomTag DICOM_TAG_RETRIEVE_URL(0x0008, 0x1190); + // Tags for Worklists + static const DicomTag DICOM_TAG_SCHEDULED_PROCEDURE_STEP_SEQUENCE(0x0040, 0x0100); + static const DicomTag DICOM_TAG_SCHEDULED_STATION_AETITLE(0x0040, 0x0001); + static const DicomTag DICOM_TAG_SCHEDULED_PROCEDURE_STEP_START_DATE(0x0040, 0x0002); }
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -388,8 +388,8 @@ LST_HEAD **l = ¶ms_->DULparams.acceptedPresentationContext; if (*l != NULL) { - DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l); - LST_Position(l, (LST_NODE*)pc); + DUL_PRESENTATIONCONTEXT* pc = reinterpret_cast<DUL_PRESENTATIONCONTEXT*>(LST_Head(l)); + LST_Position(l, reinterpret_cast<LST_NODE*>(pc)); while (pc) { if (pc->result == ASC_P_ACCEPTANCE && strlen(pc->abstractSyntax) > 0) @@ -409,7 +409,7 @@ } } - pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l); + pc = reinterpret_cast<DUL_PRESENTATIONCONTEXT*>(LST_Next(l)); } }
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -412,6 +412,10 @@ DicomControlUserConnection::IProgressListener* listener = reinterpret_cast<DicomControlUserConnection::IProgressListener*>(callbackData); if (listener) { + OFString str; + CLOG(TRACE, DICOM) << "Received Move Progress:" << std::endl + << DIMSE_dumpMessage(str, *response, DIMSE_INCOMING); + listener->OnProgressUpdated(response->NumberOfRemainingSubOperations, response->NumberOfCompletedSubOperations, response->NumberOfFailedSubOperations, @@ -422,7 +426,8 @@ void DicomControlUserConnection::MoveInternal(const std::string& targetAet, ResourceType level, - const DicomMap& fields) + const DicomMap& fields, + uint16_t messageId) { assert(association_.get() != NULL); association_->Open(parameters_); @@ -444,7 +449,15 @@ T_DIMSE_C_MoveRQ request; memset(&request, 0, sizeof(request)); - request.MessageID = association_->GetDcmtkAssociation().nextMsgID++; + if (messageId == 0) + { + request.MessageID = association_->GetDcmtkAssociation().nextMsgID++; + } + else + { + request.MessageID = messageId; + } + strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); request.Priority = DIMSE_PRIORITY_MEDIUM; request.DataSetType = DIMSE_DATASET_PRESENT; @@ -565,7 +578,7 @@ } T_DIMSE_Message msgGetRequest; - memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest)); + memset(reinterpret_cast<void*>(&msgGetRequest), 0, sizeof(msgGetRequest)); msgGetRequest.CommandField = DIMSE_C_GET_RQ; T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ); @@ -604,7 +617,7 @@ { T_DIMSE_Message rsp; // Make sure everything is zeroed (especially options) - memset((char*)&rsp, 0, sizeof(rsp)); + memset(reinterpret_cast<void*>(&rsp), 0, sizeof(rsp)); // DcmDataset* statusDetail = NULL; T_ASC_PresentationContextID cmdPresId = 0; @@ -698,7 +711,7 @@ // send the Store response T_DIMSE_Message storeResponse; - memset((char*)&storeResponse, 0, sizeof(storeResponse)); + memset(reinterpret_cast<void*>(&storeResponse), 0, sizeof(storeResponse)); storeResponse.CommandField = DIMSE_C_STORE_RSP; T_DIMSE_C_StoreRSP& storeRsp = storeResponse.msg.CStoreRSP; @@ -903,7 +916,8 @@ void DicomControlUserConnection::Move(const std::string& targetAet, ResourceType level, - const DicomMap& moveQuery) + const DicomMap& moveQuery, + uint16_t messageId) { DicomMap move; switch (level) @@ -931,12 +945,13 @@ throw OrthancException(ErrorCode_InternalError); } - MoveInternal(targetAet, level, move); + MoveInternal(targetAet, level, move, messageId); } void DicomControlUserConnection::Move(const std::string& targetAet, - const DicomMap& moveQuery) + const DicomMap& moveQuery, + uint16_t messageId) { if (!moveQuery.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) { @@ -946,49 +961,7 @@ const std::string tmp = moveQuery.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); ResourceType level = StringToResourceType(tmp.c_str()); - Move(targetAet, level, moveQuery); - } - - - void DicomControlUserConnection::MovePatient(const std::string& targetAet, - const std::string& patientId) - { - DicomMap query; - query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false); - MoveInternal(targetAet, ResourceType_Patient, query); - } - - - void DicomControlUserConnection::MoveStudy(const std::string& targetAet, - const std::string& studyUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - MoveInternal(targetAet, ResourceType_Study, query); - } - - - void DicomControlUserConnection::MoveSeries(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); - MoveInternal(targetAet, ResourceType_Series, query); - } - - - void DicomControlUserConnection::MoveInstance(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid, - const std::string& instanceUid) - { - DicomMap query; - query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); - query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); - query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false); - MoveInternal(targetAet, ResourceType_Instance, query); + Move(targetAet, level, moveQuery, messageId); }
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h Wed Nov 12 19:06:12 2025 +0100 @@ -90,7 +90,8 @@ void MoveInternal(const std::string& targetAet, ResourceType level, - const DicomMap& fields); + const DicomMap& fields, + uint16_t messageId); public: explicit DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation); @@ -126,26 +127,13 @@ void Move(const std::string& targetAet, ResourceType level, - const DicomMap& moveQuery); + const DicomMap& moveQuery, + uint16_t messageId = 0); void Move(const std::string& targetAet, - const DicomMap& moveQuery); + const DicomMap& moveQuery, + uint16_t messageId = 0); - void MovePatient(const std::string& targetAet, - const std::string& patientId); - - void MoveStudy(const std::string& targetAet, - const std::string& studyUid); - - void MoveSeries(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid); - - void MoveInstance(const std::string& targetAet, - const std::string& studyUid, - const std::string& seriesUid, - const std::string& instanceUid); - void FindWorklist(DicomFindAnswers& result, ParsedDicomFile& query); };
--- a/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -538,12 +538,12 @@ "No path to the private key for the default DICOM TLS certificate was provided"); } - if (!SystemToolbox::IsRegularFile(privateKeyPath)) + if (!SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(privateKeyPath))) { throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + privateKeyPath); } - if (!SystemToolbox::IsRegularFile(certificatePath)) + if (!SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(certificatePath))) { throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + certificatePath); } @@ -576,7 +576,7 @@ { CLOG(INFO, DICOM) << "Setting the trusted certificates for DICOM SCP connections: " << path; - if (!SystemToolbox::IsRegularFile(path)) + if (!SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(path))) { throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + path); }
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h Wed Nov 12 19:06:12 2025 +0100 @@ -44,6 +44,8 @@ virtual uint16_t Handle(DcmDataset& dicom, const std::string& remoteIp, const std::string& remoteAet, - const std::string& calledAet) = 0; + const std::string& calledAet, + uint16_t originatorMessageId, + const std::string& originatorAet) = 0; }; }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h Wed Nov 12 19:06:12 2025 +0100 @@ -62,9 +62,9 @@ unsigned int maximumPduLength, IApplicationEntityFilter* filter); - virtual ~CommandDispatcher(); + virtual ~CommandDispatcher() ORTHANC_OVERRIDE; - virtual bool Step(); + virtual bool Step() ORTHANC_OVERRIDE; }; CommandDispatcher* AcceptAssociation(const DicomServer& server,
--- a/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -112,13 +112,13 @@ // in case the sequence attribute contains exactly one item with an empty // ReferencedSOPClassUID and an empty ReferencedSOPInstanceUID, remove the item if( dataset->findAndGetElement( sequenceTagKey, sequenceAttribute ).good() && - ( (DcmSequenceOfItems*)sequenceAttribute )->card() == 1 && - ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPClassUID, referencedSOPClassUIDAttribute ).good() && + ( reinterpret_cast<DcmSequenceOfItems*>(sequenceAttribute) )->card() == 1 && + ( reinterpret_cast<DcmSequenceOfItems*>(sequenceAttribute) )->getItem(0)->findAndGetElement( DCM_ReferencedSOPClassUID, referencedSOPClassUIDAttribute ).good() && referencedSOPClassUIDAttribute->getLength() == 0 && - ( (DcmSequenceOfItems*)sequenceAttribute )->getItem(0)->findAndGetElement( DCM_ReferencedSOPInstanceUID, referencedSOPInstanceUIDAttribute, OFFalse ).good() && + ( reinterpret_cast<DcmSequenceOfItems*>(sequenceAttribute) )->getItem(0)->findAndGetElement( DCM_ReferencedSOPInstanceUID, referencedSOPInstanceUIDAttribute, OFFalse ).good() && referencedSOPInstanceUIDAttribute->getLength() == 0 ) { - DcmItem *item = ((DcmSequenceOfItems*)sequenceAttribute)->remove( ((DcmSequenceOfItems*)sequenceAttribute)->getItem(0) ); + DcmItem *item = (reinterpret_cast<DcmSequenceOfItems*>(sequenceAttribute))->remove( (reinterpret_cast<DcmSequenceOfItems*>(sequenceAttribute))->getItem(0) ); delete item; } }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -184,7 +184,7 @@ { try { - rsp->DimseStatus = cbdata->handler->Handle(**imageDataSet, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET); + rsp->DimseStatus = cbdata->handler->Handle(**imageDataSet, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET, req->MoveOriginatorID, req->MoveOriginatorApplicationEntityTitle); } catch (OrthancException& e) {
--- a/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -2019,13 +2019,13 @@ } else { - ok = element.putUint16Array((const Uint16*) decoded->c_str(), decoded->size() / sizeof(Uint16)).good(); + ok = element.putUint16Array(reinterpret_cast<const Uint16*>(decoded->c_str()), decoded->size() / sizeof(Uint16)).good(); } break; default: - ok = element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good(); + ok = element.putUint8Array(reinterpret_cast<const Uint8*>(decoded->c_str()), decoded->size()).good(); break; }
--- a/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -428,19 +428,36 @@ lutBlue == NULL || pixelData == NULL) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, "Inconsistent Palette Color Lookup Table"); } switch (format) { case PixelFormat_RGB24: { - if (r != "256\\0\\16" || - rc != 256 || - gc != 256 || - bc != 256) + std::vector<std::string> splitR; + Toolbox::SplitString(splitR, r.c_str(), '\\'); + if (splitR.size() != 3) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, std::string("Palette Color Lookup Table Descriptor invalid length: '") + r.c_str() + "'"); + } + + const unsigned int paletteSize = boost::lexical_cast<unsigned int>(splitR[0]); + const unsigned int firstInputValueMapped = boost::lexical_cast<unsigned int>(splitR[1]); + const unsigned int nbBitsUsedInLut = boost::lexical_cast<unsigned int>(splitR[2]); // The LUT is always 16bits but only part of it might be used + if (firstInputValueMapped != 0 || + nbBitsUsedInLut < 8) + { + throw OrthancException(ErrorCode_NotImplemented, "Inconsistent Palette Color Lookup Table"); + } + + const unsigned int offsetBits = nbBitsUsedInLut - 8; // Since the LUT is 16bits and the target color value is 8bpp + + if (rc != paletteSize || + gc != paletteSize || + bc != paletteSize) + { + throw OrthancException(ErrorCode_NotImplemented, std::string("Palette Color Lookup Table Descriptor invalid palette size: '") + r.c_str() + "'"); } if (pixelLength != target->GetWidth() * target->GetHeight()) @@ -477,9 +494,9 @@ for (unsigned int x = 0; x < width; x++) { - p[0] = lutRed[*source] >> 8; - p[1] = lutGreen[*source] >> 8; - p[2] = lutBlue[*source] >> 8; + p[0] = lutRed[*source] >> offsetBits; + p[1] = lutGreen[*source] >> offsetBits; + p[2] = lutBlue[*source] >> offsetBits; source++; p += 3; } @@ -496,7 +513,7 @@ bc != 65536 || pixelLength != 2 * target->GetWidth() * target->GetHeight()) { - throw OrthancException(ErrorCode_NotImplemented); + throw OrthancException(ErrorCode_NotImplemented, std::string("Palette Color Lookup Table Descriptor not supported: '") + r.c_str() + "'"); } const uint16_t* source = reinterpret_cast<const uint16_t*>(pixelData);
--- a/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -195,7 +195,7 @@ << "\" content type"; std::string content; - SystemToolbox::ReadFile(content, GetPath(uuid).string()); + SystemToolbox::ReadFile(content, GetPath(uuid)); LOG(INFO) << "Read attachment \"" << uuid << "\" (" << timer.GetHumanTransferSpeed(true, content.size()) << ")"; @@ -239,7 +239,7 @@ { for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current) { - if (SystemToolbox::IsRegularFile(current->path().string())) + if (SystemToolbox::IsRegularFile(current->path())) { try {
--- a/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h Wed Nov 12 19:06:12 2025 +0100 @@ -42,7 +42,7 @@ Content content_; public: - virtual ~MemoryStorageArea(); + virtual ~MemoryStorageArea() ORTHANC_OVERRIDE; virtual void Create(const std::string& uuid, const void* content,
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -976,7 +976,7 @@ stateMachine_.AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\""); } - stateMachine_.StartStream(contentType.c_str()); + stateMachine_.StartStream(contentType); while (stream.ReadNextChunk()) { @@ -988,7 +988,7 @@ void HttpOutput::StartStream(const std::string& contentType) { - stateMachine_.StartStream(contentType.c_str()); + stateMachine_.StartStream(contentType); } void HttpOutput::SendStreamItem(const void* data,
--- a/OrthancFramework/Sources/Images/Font.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Images/Font.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -142,7 +142,7 @@ #if ORTHANC_SANDBOXED == 0 - void Font::LoadFromFile(const std::string& path) + void Font::LoadFromFile(const boost::filesystem::path& path) { std::string font; SystemToolbox::ReadFile(font, path);
--- a/OrthancFramework/Sources/Images/Font.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Images/Font.h Wed Nov 12 19:06:12 2025 +0100 @@ -33,6 +33,11 @@ #include <map> #include <boost/noncopyable.hpp> +#if ORTHANC_SANDBOXED == 0 +# include <boost/filesystem.hpp> +#endif + + namespace Orthanc { class ORTHANC_PUBLIC Font : public boost::noncopyable @@ -74,7 +79,7 @@ void LoadFromMemory(const std::string& font); #if ORTHANC_SANDBOXED == 0 - void LoadFromFile(const std::string& path); + void LoadFromFile(const boost::filesystem::path& path); #endif const std::string& GetName() const;
--- a/OrthancFramework/Sources/Images/FontRegistry.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Images/FontRegistry.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -49,7 +49,7 @@ #if ORTHANC_SANDBOXED == 0 - void FontRegistry::AddFromFile(const std::string& path) + void FontRegistry::AddFromFile(const boost::filesystem::path& path) { std::unique_ptr<Font> f(new Font); f->LoadFromFile(path);
--- a/OrthancFramework/Sources/Images/FontRegistry.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Images/FontRegistry.h Wed Nov 12 19:06:12 2025 +0100 @@ -41,7 +41,7 @@ void AddFromMemory(const std::string& font); #if ORTHANC_SANDBOXED == 0 - void AddFromFile(const std::string& path); + void AddFromFile(const boost::filesystem::path& path); #endif size_t GetSize() const;
--- a/OrthancFramework/Sources/Images/ImageProcessing.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -2161,7 +2161,7 @@ PolygonEdge edge(v1, v2, GetPolygonNextY(points, i)); globalEdgeTable[v1.GetY()].push_back(edge); } - else if (v1.GetY() > v2.GetY()) + else { // Down-going edge PolygonEdge edge(v2, v1, yPrev);
--- a/OrthancFramework/Sources/Images/PamReader.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Images/PamReader.h Wed Nov 12 19:06:12 2025 +0100 @@ -73,7 +73,7 @@ */ explicit PamReader(bool enforceAligned); - virtual ~PamReader(); + virtual ~PamReader() ORTHANC_OVERRIDE; #if ORTHANC_SANDBOXED == 0 void ReadFromFile(const boost::filesystem::path& filename);
--- a/OrthancFramework/Sources/Images/PngReader.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Images/PngReader.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -102,9 +102,9 @@ } endInfo_ = png_create_info_struct(png_); - if (!info_) + if (!endInfo_) { - png_destroy_read_struct(&png_, &info_, NULL); + png_destroy_read_struct(&png_, &endInfo_, NULL); throw OrthancException(ErrorCode_NotEnoughMemory); } }
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -375,8 +375,8 @@ }; - bool JobsRegistry::PriorityComparator::operator() (JobHandler* const& a, - JobHandler* const& b) const + bool JobsRegistry::PriorityComparator::operator() (const JobHandler* const& a, + const JobHandler* const& b) const { return a->GetPriority() < b->GetPriority(); } @@ -404,7 +404,7 @@ return false; } - bool JobsRegistry::IsCompletedJob(JobHandler& job) const + bool JobsRegistry::IsCompletedJob(const JobHandler& job) const { for (CompletedJobs::const_iterator it = completedJobs_.begin(); it != completedJobs_.end(); ++it) @@ -918,12 +918,7 @@ { // Success, try and retrieve the status of the job JobsIndex::const_iterator it = jobsIndex_.find(id); - if (it == jobsIndex_.end()) - { - // Should not happen - state = JobState_Failure; - } - else + if (it != jobsIndex_.end()) { const JobStatus& status = it->second->GetLastStatus(); successContent = status.GetPublicContent(); @@ -1597,7 +1592,7 @@ for (JobsIndex::const_iterator it = jobsIndex_.begin(); it != jobsIndex_.end(); ++it) { - JobHandler& job = *it->second; + const JobHandler& job = *it->second; switch (job.GetState()) {
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h Wed Nov 12 19:06:12 2025 +0100 @@ -73,8 +73,8 @@ struct PriorityComparator { - bool operator() (JobHandler* const& a, - JobHandler* const& b) const; + bool operator() (const JobHandler* const& a, + const JobHandler* const& b) const; }; typedef std::map<std::string, JobHandler*> JobsIndex; @@ -100,7 +100,7 @@ #ifndef NDEBUG bool IsPendingJob(const JobHandler& job) const; - bool IsCompletedJob(JobHandler& job) const; + bool IsCompletedJob(const JobHandler& job) const; bool IsRetryJob(JobHandler& job) const; #endif
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -460,7 +460,7 @@ done_(false) { std::string jobType; - GetJobType(jobType); + SequenceOfOperationsJob::GetJobType(jobType); if (SerializationToolbox::ReadString(serialized, TYPE) != jobType || !serialized.isMember(OPERATIONS) ||
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h Wed Nov 12 19:06:12 2025 +0100 @@ -70,7 +70,7 @@ SequenceOfOperationsJob(IJobUnserializer& unserializer, const Json::Value& serialized); - virtual ~SequenceOfOperationsJob(); + virtual ~SequenceOfOperationsJob() ORTHANC_OVERRIDE; void SetDescription(const std::string& description);
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h Wed Nov 12 19:06:12 2025 +0100 @@ -71,7 +71,7 @@ SetOfCommandsJob(ICommandUnserializer* unserializer /* takes ownership */, const Json::Value& source); - virtual ~SetOfCommandsJob(); + virtual ~SetOfCommandsJob() ORTHANC_OVERRIDE; size_t GetPosition() const; @@ -79,7 +79,7 @@ const std::string& GetDescription() const; - void Reserve(size_t size); + virtual void Reserve(size_t size); size_t GetCommandsCount() const;
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -106,7 +106,7 @@ { } - virtual ICommand* Unserialize(const Json::Value& source) const + virtual ICommand* Unserialize(const Json::Value& source) const ORTHANC_OVERRIDE { if (source.type() == Json::nullValue) {
--- a/OrthancFramework/Sources/Logging.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Logging.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -30,6 +30,10 @@ #include <cassert> #include <stdint.h> +#if defined(__linux__) && !defined(NDEBUG) +# include <pthread.h> +#endif + /********************************************************* * Common section @@ -596,7 +600,7 @@ **/ boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); - boost::filesystem::path root(directory); + boost::filesystem::path root(SystemToolbox::PathFromUtf8(directory)); boost::filesystem::path exe(SystemToolbox::GetPathToExecutable()); if (!boost::filesystem::exists(root) || @@ -658,6 +662,11 @@ } threadNames_[id] = name; + +#if defined(__linux__) && !defined(NDEBUG) && !defined(__LSB_VERSION__) + // set the thread name at "system" level too -> required to have the thread names visible in GDB ! + pthread_setname_np(pthread_self(), name.substr(0, 15).c_str()); // thread names are limited to 15 in Linux +#endif } void SetCurrentThreadName(const std::string& name)
--- a/OrthancFramework/Sources/MallocMemoryBuffer.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/MallocMemoryBuffer.h Wed Nov 12 19:06:12 2025 +0100 @@ -45,7 +45,7 @@ public: MallocMemoryBuffer(); - virtual ~MallocMemoryBuffer() + virtual ~MallocMemoryBuffer() ORTHANC_OVERRIDE { Clear(); }
--- a/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h Wed Nov 12 19:06:12 2025 +0100 @@ -25,13 +25,14 @@ #pragma once #include "../IDynamicObject.h" +#include "../Compatibility.h" namespace Orthanc { class IRunnableBySteps : public IDynamicObject { public: - virtual ~IRunnableBySteps() + virtual ~IRunnableBySteps() ORTHANC_OVERRIDE { }
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -169,9 +169,9 @@ } - void RestApiHierarchy::DeleteChildren(Children& children) + void RestApiHierarchy::DeleteChildren(const Children& children) { - for (Children::iterator it = children.begin(); + for (Children::const_iterator it = children.begin(); it != children.end(); ++it) { delete it->second;
--- a/OrthancFramework/Sources/RestApi/RestApiHierarchy.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h Wed Nov 12 19:06:12 2025 +0100 @@ -97,7 +97,7 @@ static RestApiHierarchy& AddChild(Children& children, const std::string& name); - static void DeleteChildren(Children& children); + static void DeleteChildren(const Children& children); template <typename Handler> void RegisterInternal(const RestApiPath& path,
--- a/OrthancFramework/Sources/SystemToolbox.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -268,36 +268,6 @@ } } - void SystemToolbox::ReadFile(std::string &content, - const std::string& path, - bool log) - { - ReadFile(content, PathFromUtf8(path), log); - } - - void SystemToolbox::ReadFile(std::string& content, const boost::filesystem::path& path) - { - ReadFile(content, path, true /* log */); - } - - - void SystemToolbox::ReadFile(std::string &content, const std::string &path) - { - ReadFile(content, PathFromUtf8(path), true /* log */); - } - - void SystemToolbox::ReadFile(std::string &content, const char* path) - { - ReadFile(content, std::string(path)); - } - - bool SystemToolbox::ReadHeader(std::string& header, - const std::string& path, - size_t headerSize) - { - return ReadHeader(header, PathFromUtf8(path), headerSize); - } - bool SystemToolbox::ReadHeader(std::string& header, const boost::filesystem::path& path, @@ -355,18 +325,9 @@ } - - void SystemToolbox::WriteFile(const void* content, - size_t size, - const std::string& path, - bool callFsync) - { - WriteFile(content, size, PathFromUtf8(path), callFsync); - } - void SystemToolbox::WriteFile(const void *content, size_t size, - const boost::filesystem::path &path, + const boost::filesystem::path& path, bool callFsync) { try @@ -432,35 +393,7 @@ } - void SystemToolbox::WriteFile(const void *content, size_t size, const std::string &path) - { - WriteFile(content, size, path, false /* don't automatically call fsync */); - } - - void SystemToolbox::WriteFile(const std::string& content, - const std::string& path, - bool callFsync) - { - WriteFile(content.size() > 0 ? content.c_str() : NULL, - content.size(), path, callFsync); - } - - void SystemToolbox::WriteFile(const std::string &content, - const boost::filesystem::path& path, - bool callFsync) - { - WriteFile(content.size() > 0 ? content.c_str() : NULL, - content.size(), path, callFsync); - } - - void SystemToolbox::WriteFile(const std::string &content, const std::string &path) - { - WriteFile(content, path, false /* don't automatically call fsync */); - } - - - void SystemToolbox::WriteFile(const std::string &content, const boost::filesystem::path& path) { WriteFile(content.size() > 0 ? content.c_str() : NULL, @@ -468,7 +401,7 @@ } - void SystemToolbox::RemoveFile(const std::string& path) + void SystemToolbox::RemoveFile(const boost::filesystem::path& path) { if (boost::filesystem::exists(path)) { @@ -483,10 +416,6 @@ } } - uint64_t SystemToolbox::GetFileSize(const std::string &path) - { - return GetFileSize(PathFromUtf8(path)); - } uint64_t SystemToolbox::GetFileSize(const boost::filesystem::path& path) { @@ -504,6 +433,7 @@ } } + #if ORTHANC_ENABLE_MD5 == 1 void SystemToolbox::ComputeStreamMD5(std::string& result, std::istream& inputStream) @@ -528,13 +458,6 @@ } - void SystemToolbox::ComputeFileMD5(std::string& result, - const std::string& path) - { - return ComputeFileMD5(result, PathFromUtf8(path)); - } - - void SystemToolbox::ComputeFileMD5(std::string& result, const boost::filesystem::path& path) { @@ -549,12 +472,9 @@ ComputeStreamMD5(result, fileStream); } - bool SystemToolbox::CompareFilesMD5(const std::string &path1, const std::string &path2) - { - return CompareFilesMD5(PathFromUtf8(path1), PathFromUtf8(path2)); - } - bool SystemToolbox::CompareFilesMD5(const boost::filesystem::path& path1, const boost::filesystem::path &path2) + bool SystemToolbox::CompareFilesMD5(const boost::filesystem::path& path1, + const boost::filesystem::path& path2) { if (GetFileSize(path1) != GetFileSize(path2)) { @@ -573,11 +493,6 @@ #endif - void SystemToolbox::MakeDirectory(const std::string& path) - { - MakeDirectory(PathFromUtf8(path)); - } - void SystemToolbox::MakeDirectory(const boost::filesystem::path& path) { if (boost::filesystem::exists(path)) @@ -597,12 +512,6 @@ } - bool SystemToolbox::IsExistingFile(const std::string& path) - { - return boost::filesystem::exists(path); - } - - #if defined(_WIN32) static std::string GetPathToExecutableInternal() { @@ -676,7 +585,7 @@ #ifdef _WIN32 - std::wstring SystemToolbox::Utf8ToWString(const std::string &str) + std::wstring SystemToolbox::Utf8ToWString(const std::string& str) { if (str.empty()) { @@ -723,16 +632,16 @@ } #endif - boost::filesystem::path SystemToolbox::PathFromUtf8(const std::string &utf8) + boost::filesystem::path SystemToolbox::PathFromUtf8(const std::string& utf8) { #if defined(_WIN32) && !defined(__MINGW32__) // non-ASCII paths are not supported when building with MinGW -return boost::filesystem::path(Utf8ToWString(utf8)); + return boost::filesystem::path(Utf8ToWString(utf8)); #else return boost::filesystem::path(utf8); // POSIX: std::string is UTF-8 #endif } - std::string SystemToolbox::PathToUtf8(const boost::filesystem::path &p) + std::string SystemToolbox::PathToUtf8(const boost::filesystem::path& p) { #if defined(_WIN32) && !defined(__MINGW32__) // non-ASCII paths are not supported when building with MinGW return WStringToUtf8(p.wstring()); @@ -741,17 +650,18 @@ #endif } - std::string SystemToolbox::GetPathToExecutable() + + boost::filesystem::path SystemToolbox::GetPathToExecutable() { - boost::filesystem::path p(GetPathToExecutableInternal()); - return boost::filesystem::absolute(p).string(); + boost::filesystem::path p(PathFromUtf8(GetPathToExecutableInternal())); + return boost::filesystem::absolute(p); } - std::string SystemToolbox::GetDirectoryOfExecutable() + boost::filesystem::path SystemToolbox::GetDirectoryOfExecutable() { - boost::filesystem::path p(GetPathToExecutableInternal()); - return boost::filesystem::absolute(p.parent_path()).string(); + boost::filesystem::path p(PathFromUtf8(GetPathToExecutableInternal())); + return boost::filesystem::absolute(p.parent_path()); } @@ -837,18 +747,6 @@ } - bool SystemToolbox::IsRegularFile(const std::string& path) - { - return IsRegularFile(PathFromUtf8(path)); - } - - - FILE *SystemToolbox::OpenFile(const std::string &path, - FileMode mode) - { - return OpenFile(PathFromUtf8(path), mode); - } - FILE* SystemToolbox::OpenFile(const boost::filesystem::path& path, FileMode mode) { @@ -989,11 +887,6 @@ return false; } - MimeType SystemToolbox::AutodetectMimeType(const std::string &path) - { - return AutodetectMimeType(PathFromUtf8(path)); - } - MimeType SystemToolbox::AutodetectMimeType(const boost::filesystem::path& path) { @@ -1136,41 +1029,29 @@ boost::filesystem::path SystemToolbox::InterpretRelativePath(const boost::filesystem::path& baseDirectory, - const std::string& relativePath) + const boost::filesystem::path& relativePath) { - boost::filesystem::path relative = SystemToolbox::PathFromUtf8(relativePath); - /** The following lines should be equivalent to this one: - return (base / relative); + return (baseDirectory / relativePath); However, for some unknown reason, some versions of Boost do not make the proper path resolution when "baseDirectory" is an absolute path. So, a hack is used below. **/ - if (relative.is_absolute()) + if (relativePath.is_absolute()) { - return relative; + return relativePath; } else { - return baseDirectory / relative; + return baseDirectory / relativePath; } } - void SystemToolbox::ReadFileRange(std::string &content, - const std::string &path, - uint64_t start, // Inclusive - uint64_t end, // Exclusive - bool throwIfOverflow) - { - ReadFileRange(content, SystemToolbox::PathFromUtf8(path), start, end, throwIfOverflow); - } - - void SystemToolbox::ReadFileRange(std::string& content, const boost::filesystem::path& path, uint64_t start, // Inclusive
--- a/OrthancFramework/Sources/SystemToolbox.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/SystemToolbox.h Wed Nov 12 19:06:12 2025 +0100 @@ -46,6 +46,9 @@ #include <stdint.h> #include <boost/filesystem.hpp> +// Note: The use of "boost::filesystem::path" is mandatory to handle +// non ASCII-only path on Windows + namespace Orthanc { class ORTHANC_PUBLIC SystemToolbox @@ -58,119 +61,65 @@ static ServerBarrierEvent ServerBarrier(); static void ReadFile(std::string& content, - const std::string& path, - bool log); - - static void ReadFile(std::string &content, const boost::filesystem::path& path, bool log); - static void ReadFile(std::string &content, - const char* path); - - static void ReadFile(std::string& content, - const std::string& path); - - static void ReadFile(std::string &content, - const boost::filesystem::path& path); + static inline void ReadFile(std::string& content, + const boost::filesystem::path& path) + { + ReadFile(content, path, true /* log */); + } static bool ReadHeader(std::string& header, - const std::string& path, + const boost::filesystem::path& path, size_t headerSize); - static bool ReadHeader(std::string &header, - const boost::filesystem::path& path, - size_t headerSize); - static void WriteFile(const void* content, size_t size, - const std::string& path, - bool callFsync); - - static void WriteFile(const void *content, - size_t size, - const boost::filesystem::path &path, - bool callFsync); // this variant is mandatory to handle non ASCII-only path on Windows - - static void WriteFile(const void* content, - size_t size, - const std::string& path); - - static void WriteFile(const std::string& content, - const std::string& path, + const boost::filesystem::path& path, bool callFsync); static void WriteFile(const std::string& content, - const std::string& path); - - static void WriteFile(const std::string &content, const boost::filesystem::path& path); - static void WriteFile(const std::string &content, - const boost::filesystem::path& path, - bool callFsync); + static void RemoveFile(const boost::filesystem::path& path); - static void RemoveFile(const std::string& path); - - static uint64_t GetFileSize(const std::string& path); - - static uint64_t GetFileSize(const boost::filesystem::path &path); // this variant is mandatory to handle non ASCII-only path on Windows + static uint64_t GetFileSize(const boost::filesystem::path& path); #if ORTHANC_ENABLE_MD5 == 1 static void ComputeStreamMD5(std::string& result, std::istream& stream); static void ComputeFileMD5(std::string& result, - const std::string& path); - - static void ComputeFileMD5(std::string &result, - const boost::filesystem::path& path); // this variant is mandatory to handle non ASCII-only path on Windows + const boost::filesystem::path& path); - // returns true if file have the same MD5 - static bool CompareFilesMD5(const std::string& path1, - const std::string& path2); - + // Returns true if the two files have the same MD5 static bool CompareFilesMD5(const boost::filesystem::path& path1, - const boost::filesystem::path& path2); // this variant is mandatory to handle non ASCII-only path on Windows + const boost::filesystem::path& path2); #endif - static void MakeDirectory(const std::string& path); - static void MakeDirectory(const boost::filesystem::path& path); - static bool IsExistingFile(const std::string& path); + static boost::filesystem::path GetPathToExecutable(); - static std::string GetPathToExecutable(); - - static std::string GetDirectoryOfExecutable(); + static boost::filesystem::path GetDirectoryOfExecutable(); static void ExecuteSystemCommand(const std::string& command, const std::vector<std::string>& arguments); static int GetProcessId(); - static bool IsRegularFile(const std::string& path); - - static bool IsRegularFile(const boost::filesystem::path& path); // this variant is mandatory to handle non ASCII-only path on Windows + static bool IsRegularFile(const boost::filesystem::path& path); - static FILE* OpenFile(const std::string& path, - FileMode mode); - - static FILE* OpenFile(const boost::filesystem::path& path, + static FILE* OpenFile(const boost::filesystem::path& path, FileMode mode); - static MimeType AutodetectMimeType(const char* path) // used only in Unit Tests - { - return AutodetectMimeType(std::string(path)); - } + static MimeType AutodetectMimeType(const boost::filesystem::path& path); - static MimeType AutodetectMimeType(const std::string &path); - - static MimeType AutodetectMimeType(const boost::filesystem::path &path); + // This conversion is mandatory to handle non ASCII-only path on Windows + static boost::filesystem::path PathFromUtf8(const std::string& utf8); - static boost::filesystem::path PathFromUtf8(const std::string &utf8); - - static std::string PathToUtf8(const boost::filesystem::path &p); + static std::string PathToUtf8(const boost::filesystem::path& p); static std::string GetNowIsoString(bool utc); @@ -187,15 +136,9 @@ static void GetEnvironmentVariables(std::map<std::string, std::string>& env); static boost::filesystem::path InterpretRelativePath(const boost::filesystem::path& baseDirectory, - const std::string& relativePath); + const boost::filesystem::path& relativePath); - static void ReadFileRange(std::string& content, - const std::string& path, - uint64_t start, // Inclusive - uint64_t end, // Exclusive - bool throwIfOverflow); - - static void ReadFileRange(std::string &content, + static void ReadFileRange(std::string& content, const boost::filesystem::path& path, uint64_t start, // Inclusive uint64_t end, // Exclusive @@ -204,9 +147,9 @@ static void GetMacAddresses(std::set<std::string>& target); #ifdef _WIN32 - static std::wstring Utf8ToWString(const std::string &str); + static std::wstring Utf8ToWString(const std::string& str); - static std::string WStringToUtf8(const std::wstring &wstr); + static std::string WStringToUtf8(const std::wstring& wstr); static std::wstring WStringFromCharPtr(const char *wstr); #endif
--- a/OrthancFramework/Sources/Toolbox.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/Sources/Toolbox.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -1884,7 +1884,7 @@ #if defined(__unix__) && ORTHANC_SANDBOXED != 1 static const char* LOCALTIME = "/etc/localtime"; - if (!SystemToolbox::IsExistingFile(LOCALTIME)) + if (!boost::filesystem::exists(LOCALTIME)) { // Check out file // "boost_1_69_0/libs/locale/src/icu/time_zone.cpp": Direct @@ -2935,17 +2935,10 @@ else if (ORTHANC_SCANF(version, "%4d", &a) == 1 && a >= 0) { - if (a >= 0) - { - major = static_cast<unsigned int>(a); - minor = 0; - revision = 0; - return true; - } - else - { - return false; - } + major = static_cast<unsigned int>(a); + minor = 0; + revision = 0; + return true; } else {
--- a/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/DicomMapTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -1466,7 +1466,7 @@ { std::string dicom; - SystemToolbox::ReadFile(dicom, path); + SystemToolbox::ReadFile(dicom, current->path()); ParsedDicomFile f(dicom); f.ExtractDicomSummary(m2, 256);
--- a/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/FileStorageTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -59,7 +59,7 @@ std::string data = Toolbox::GenerateUuid(); std::string uid = Toolbox::GenerateUuid(); - s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); + s.Create(uid, &data[0], data.size(), FileContentType_Unknown); std::string d; { std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); @@ -83,7 +83,7 @@ std::vector<uint8_t> data; StringToVector(data, Toolbox::GenerateUuid()); std::string uid = Toolbox::GenerateUuid(); - s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown); + s.Create(uid, &data[0], data.size(), FileContentType_Unknown); std::string d; { std::unique_ptr<IMemoryBuffer> buffer(s.ReadWhole(uid, FileContentType_Unknown)); @@ -167,7 +167,7 @@ { std::string t = Toolbox::GenerateUuid(); std::string uid = Toolbox::GenerateUuid(); - s.Create(uid.c_str(), &t[0], t.size(), FileContentType_Unknown); + s.Create(uid, &t[0], t.size(), FileContentType_Unknown); u.push_back(uid); } @@ -354,4 +354,46 @@ ASSERT_EQ("o", s); ASSERT_THROW(StorageAccessor::Range::ParseHttpRange("bytes=5-").Extract(s, "Hello"), OrthancException); } -} \ No newline at end of file +} + + +TEST(SystemToolbox, ReadRange) +{ + const boost::filesystem::path path(SystemToolbox::PathFromUtf8("UnitTestsResults/hello.txt")); + SystemToolbox::WriteFile("abc", path); + + std::string s; + SystemToolbox::ReadFileRange(s, path, 0, 1, true); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ('a', s[0]); + + SystemToolbox::ReadFileRange(s, path, 1, 2, true); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ('b', s[0]); + + SystemToolbox::ReadFileRange(s, path, 2, 3, true); + ASSERT_EQ(1u, s.size()); + ASSERT_EQ('c', s[0]); + + ASSERT_THROW(SystemToolbox::ReadFileRange(s, path, 3, 4, true), OrthancException); + + SystemToolbox::ReadFileRange(s, path, 0, 2, true); + ASSERT_EQ(2u, s.size()); + ASSERT_EQ('a', s[0]); + ASSERT_EQ('b', s[1]); + + SystemToolbox::ReadFileRange(s, path, 1, 3, true); + ASSERT_EQ(2u, s.size()); + ASSERT_EQ('b', s[0]); + ASSERT_EQ('c', s[1]); + + ASSERT_THROW(SystemToolbox::ReadFileRange(s, path, 2, 4, true), OrthancException); + + SystemToolbox::ReadFileRange(s, path, 0, 3, true); + ASSERT_EQ(3u, s.size()); + ASSERT_EQ('a', s[0]); + ASSERT_EQ('b', s[1]); + ASSERT_EQ('c', s[2]); + + ASSERT_THROW(SystemToolbox::ReadFileRange(s, path, 1, 4, true), OrthancException); +}
--- a/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -499,7 +499,7 @@ // This is a Latin-1 test string const unsigned char data[10] = { 0xe0, 0xe9, 0xea, 0xe7, 0x26, 0xc6, 0x61, 0x62, 0x63, 0x00 }; - std::string s((char*) &data[0], 10); + std::string s(reinterpret_cast<const char*>(&data[0]), 10); ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s)); // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C" @@ -528,7 +528,7 @@ // This is a Latin-1 test string: "crane" with a circumflex accent const unsigned char latin1[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 }; - std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char)); + std::string s(reinterpret_cast<const char*>(&latin1[0]), sizeof(latin1) / sizeof(char)); ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false, false), Encoding_Latin1)); ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false, false)); @@ -539,7 +539,7 @@ size_t size, size_t expectedLength) { - std::string s((char*) &data[0], size); + std::string s(reinterpret_cast<const char*>(&data[0]), size); uint32_t unicode; size_t length; Toolbox::Utf8ToUnicodeCharacter(unicode, length, s, 0); @@ -689,7 +689,7 @@ { TemporaryFile tmp; std::string s = "Hello"; - SystemToolbox::WriteFile(s, tmp.GetPath(), true /* call fsync() */); + SystemToolbox::WriteFile(s.c_str(), s.size(), tmp.GetPath(), true /* call fsync() */); std::string t; SystemToolbox::ReadFile(t, tmp.GetPath()); ASSERT_EQ(s, t);
--- a/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -3558,12 +3558,12 @@ std::string path = (std::string(getenv("HOME")) + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + std::string(GetTransferSyntaxUid(a)) + ".dcm"); - if (Orthanc::SystemToolbox::IsRegularFile(path)) + if (Orthanc::SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(path))) { printf("\n======= %s\n", GetTransferSyntaxUid(a)); std::string source; - Orthanc::SystemToolbox::ReadFile(source, path); + Orthanc::SystemToolbox::ReadFile(source, Orthanc::SystemToolbox::PathFromUtf8(path)); ParsedDicomFile dicom(source); std::unique_ptr<DcmElement> removal(dicom.GetDcmtkObject().getDataset()->remove(DCM_PixelData)); @@ -3623,16 +3623,16 @@ std::string path = (std::string(getenv("HOME")) + "/Subversion/orthanc-tests/Database/TransferSyntaxes/" + std::string(GetTransferSyntaxUid(a)) + ".dcm"); - if (Orthanc::SystemToolbox::IsRegularFile(path)) + if (Orthanc::SystemToolbox::IsRegularFile(SystemToolbox::PathFromUtf8(path))) { printf("\n======= %s\n", GetTransferSyntaxUid(a)); std::string source; - Orthanc::SystemToolbox::ReadFile(source, path); - - std::string c, k; + Orthanc::SystemToolbox::ReadFile(source, SystemToolbox::PathFromUtf8(path)); + try { + std::string c, k; scu.Transcode(c, k, transcoder, source.c_str(), source.size(), DicomTransferSyntax_LittleEndianExplicit, false, "", 0); } @@ -3678,8 +3678,6 @@ std::set<DicomTransferSyntax> s; s.insert(a); - std::string t; - IDicomTranscoder::DicomImage source, target; source.AcquireParsed(dynamic_cast<DcmFileFormat*>(toto->clone())); @@ -3698,7 +3696,6 @@ a == DicomTransferSyntax_JPEGProcess2_4 || a == DicomTransferSyntax_JPEGLSLossy); - printf("SIZE: %d\n", static_cast<int>(t.size())); if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed())) { ASSERT_FALSE(lossy);
--- a/OrthancFramework/UnitTestsSources/ImageTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/ImageTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -77,7 +77,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/ColorPattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/ColorPattern.png")); #endif std::string md5; @@ -137,7 +137,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Color16Pattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Color16Pattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Color16Pattern.png")); #endif std::string md5; @@ -172,7 +172,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray8Pattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray8Pattern.png")); #endif std::string md5; @@ -208,7 +208,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray16Pattern.png", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray16Pattern.png")); #endif std::string md5; @@ -252,7 +252,7 @@ v = 0; for (unsigned int y = 0; y < height; y++) { - const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch()); + const uint16_t *p = reinterpret_cast<const uint16_t*>(reinterpret_cast<const uint8_t*>(r.GetConstBuffer()) + y * r.GetPitch()); ASSERT_EQ(p, r.GetConstRow(y)); for (unsigned int x = 0; x < width; x++, p++, v++) { @@ -276,7 +276,7 @@ v = 0; for (unsigned int y = 0; y < height; y++) { - const uint16_t *p = reinterpret_cast<const uint16_t*>((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch()); + const uint16_t *p = reinterpret_cast<const uint16_t*>(reinterpret_cast<const uint8_t*>(r2.GetConstBuffer()) + y * r2.GetPitch()); ASSERT_EQ(p, r2.GetConstRow(y)); for (unsigned int x = 0; x < width; x++, p++, v++) { @@ -313,7 +313,7 @@ Orthanc::SystemToolbox::WriteFile(s, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/hello2.jpg")); std::string t; - Orthanc::SystemToolbox::ReadFile(t, "UnitTestsResults/hello.jpg"); + Orthanc::SystemToolbox::ReadFile(t, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/hello.jpg")); ASSERT_EQ(s.size(), t.size()); ASSERT_EQ(0, memcmp(s.c_str(), t.c_str(), s.size())); #endif @@ -385,7 +385,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/ColorPattern.pam", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.pam"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/ColorPattern.pam")); #endif std::string md5; @@ -419,7 +419,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray8Pattern.pam", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.pam"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray8Pattern.pam")); #endif std::string md5; @@ -455,7 +455,7 @@ Orthanc::IImageWriter::WriteToMemory(w, f, accessor); #else Orthanc::IImageWriter::WriteToFile(w, "UnitTestsResults/Gray16Pattern.pam", accessor); - Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.pam"); + Orthanc::SystemToolbox::ReadFile(f, Orthanc::SystemToolbox::PathFromUtf8("UnitTestsResults/Gray16Pattern.pam")); #endif std::string md5; @@ -500,7 +500,7 @@ for (unsigned int y = 0; y < height; y++) { const uint16_t *p = reinterpret_cast<const uint16_t*> - ((const uint8_t*) r.GetConstBuffer() + y * r.GetPitch()); + (reinterpret_cast<const uint8_t*>(r.GetConstBuffer()) + y * r.GetPitch()); ASSERT_EQ(p, r.GetConstRow(y)); for (unsigned int x = 0; x < width; x++, p++, v++) { @@ -522,7 +522,7 @@ for (unsigned int y = 0; y < height; y++) { const uint16_t* p = reinterpret_cast<const uint16_t*> - ((const uint8_t*)r.GetConstBuffer() + y * r.GetPitch()); + (reinterpret_cast<const uint8_t*>(r.GetConstBuffer()) + y * r.GetPitch()); ASSERT_EQ(p, r.GetConstRow(y)); for (unsigned int x = 0; x < width; x++, p++, v++) { @@ -547,7 +547,7 @@ for (unsigned int y = 0; y < height; y++) { const uint16_t *p = reinterpret_cast<const uint16_t*> - ((const uint8_t*) r2.GetConstBuffer() + y * r2.GetPitch()); + (reinterpret_cast<const uint8_t*>(r2.GetConstBuffer()) + y * r2.GetPitch()); ASSERT_EQ(p, r2.GetConstRow(y)); for (unsigned int x = 0; x < width; x++, p++, v++) { @@ -574,7 +574,7 @@ for (unsigned int y = 0; y < height; y++) { const uint16_t* p = reinterpret_cast<const uint16_t*> - ((const uint8_t*)r2.GetConstBuffer() + y * r2.GetPitch()); + (reinterpret_cast<const uint8_t*>(r2.GetConstBuffer()) + y * r2.GetPitch()); ASSERT_EQ(p, r2.GetConstRow(y)); for (unsigned int x = 0; x < width; x++, p++, v++) {
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -282,10 +282,10 @@ TEST(MultiThreading, SharedMessageQueueClean) { - std::set<int> s; - try { + std::set<int> s; + SharedMessageQueue q; q.Enqueue(new DynamicInteger(10, s)); q.Enqueue(new DynamicInteger(20, s)); @@ -733,25 +733,26 @@ } -TEST(JobsEngine, DISABLED_SequenceOfOperationsJob) +TEST(JobsEngine, SequenceOfOperationsJob) { JobsEngine engine(10); engine.SetThreadSleep(10); engine.SetWorkersCount(3); engine.Start(); - std::string id; - SequenceOfOperationsJob* job = NULL; - { - std::unique_ptr<SequenceOfOperationsJob> a(new SequenceOfOperationsJob); - job = a.get(); - engine.GetRegistry().Submit(id, a.release(), 0); + std::string id; + std::unique_ptr<SequenceOfOperationsJob> seq(new SequenceOfOperationsJob); + engine.GetRegistry().Submit(id, seq.release(), 0); } boost::this_thread::sleep(boost::posix_time::milliseconds(500)); { + SequenceOfOperationsJob* job = NULL; + std::unique_ptr<SequenceOfOperationsJob> seq(new SequenceOfOperationsJob); + job = seq.get(); + SequenceOfOperationsJob::Lock lock(*job); size_t i = lock.AddOperation(new LogJobOperation); size_t j = lock.AddOperation(new LogJobOperation); @@ -1137,9 +1138,9 @@ TEST(JobsSerialization, Registry) { Json::Value s; - std::string i1, i2; { + std::string i1, i2; JobsRegistry registry(10); registry.Submit(i1, new DummyJob(), 10); registry.Submit(i2, new SequenceOfOperationsJob(), 30);
--- a/OrthancFramework/UnitTestsSources/RestApiTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -402,7 +402,7 @@ void Reset() { HttpContentNegociation::Dictionary parameters; - Handle("nope", "nope", parameters); + AcceptHandler::Handle("nope", "nope", parameters); } const std::string& GetType() const
--- a/OrthancFramework/UnitTestsSources/SQLiteTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancFramework/UnitTestsSources/SQLiteTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -54,7 +54,7 @@ TEST(SQLite, Connection) { - SystemToolbox::RemoveFile("UnitTestsResults/coucou"); + SystemToolbox::RemoveFile(SystemToolbox::PathFromUtf8("UnitTestsResults/coucou")); SQLite::Connection c; c.Open("UnitTestsResults/coucou"); c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)");
--- a/OrthancServer/CMakeLists.txt Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/CMakeLists.txt Wed Nov 12 19:06:12 2025 +0100 @@ -593,6 +593,7 @@ add_library(ServeFolders SHARED ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/Plugin.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ServeFolders/OrthancFrameworkDependencies.cpp ${SERVE_FOLDERS_RESOURCES} ) @@ -636,6 +637,14 @@ list(APPEND MODALITY_WORKLISTS_RESOURCES ${AUTOGENERATED_DIR}/ModalityWorklists.rc) endif() + EmbedResources( + --target=ModalityWorklistsResources + --namespace=Orthanc.FrameworkResources + --framework-path=${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources + ${LIBICU_RESOURCES} + ${DCMTK_DICTIONARIES} + ) + set_source_files_properties( ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp PROPERTIES COMPILE_DEFINITIONS "MODALITY_WORKLISTS_VERSION=\"${ORTHANC_VERSION}\"" @@ -643,12 +652,14 @@ add_library(ModalityWorklists SHARED ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/Plugin.cpp + ${CMAKE_SOURCE_DIR}/Plugins/Samples/ModalityWorklists/OrthancFrameworkDependencies.cpp + ${AUTOGENERATED_DIR}/ModalityWorklistsResources.cpp ${MODALITY_WORKLISTS_RESOURCES} ) DefineSourceBasenameForTarget(ModalityWorklists) - target_link_libraries(ModalityWorklists PluginsDependencies) + target_link_libraries(ModalityWorklists PluginsDependencies ${DCMTK_LIBRARIES}) set_target_properties( ModalityWorklists PROPERTIES
--- a/OrthancServer/OrthancExplorer/explorer.js Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/OrthancExplorer/explorer.js Wed Nov 12 19:06:12 2025 +0100 @@ -173,8 +173,8 @@ var authorizationTokens = GetAuthorizationTokensFromUrl(); -/* Copy the authoziation toekn from the url search parameters into HTTP headers in every request to the Rest API. -Thanks to this behaviour, you may specify a ?token=xxx in your url and this will be passed +/* Copy the authoziation token from the url search parameters into HTTP headers in every request to the REST API. +Thanks to this behaviour, you may specify a ?token=xxx in your url and this will be passed as the "token" header in every request to the API allowing you to use the authorization plugin */ $.ajaxSetup( {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -238,7 +238,7 @@ ResetAnswers(); } - virtual ~Transaction() + virtual ~Transaction() ORTHANC_OVERRIDE { assert(that_.activeTransaction_ != NULL); that_.activeTransaction_ = NULL;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.h Wed Nov 12 19:06:12 2025 +0100 @@ -54,7 +54,7 @@ void* database, const std::string& serverIdentifier); - virtual ~OrthancPluginDatabaseV3(); + virtual ~OrthancPluginDatabaseV3() ORTHANC_OVERRIDE; virtual void Open() ORTHANC_OVERRIDE;
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -527,7 +527,7 @@ } - virtual ~Transaction() + virtual ~Transaction() ORTHANC_OVERRIDE { try {
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed Nov 12 19:06:12 2025 +0100 @@ -53,7 +53,7 @@ const _OrthancPluginRegisterDatabaseBackendV4& database, const std::string& serverIdentifier); - virtual ~OrthancPluginDatabaseV4(); + virtual ~OrthancPluginDatabaseV4() ORTHANC_OVERRIDE; const _OrthancPluginRegisterDatabaseBackendV4& GetDefinition() const {
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -1562,7 +1562,7 @@ } } - virtual ~Handler() + virtual ~Handler() ORTHANC_OVERRIDE { assert(handler_ != NULL); parameters_.destructor(handler_); @@ -3951,17 +3951,17 @@ std::string certificate(parameters.certificateFile); std::string key, password; - if (parameters.certificateKeyFile) + if (parameters.certificateKeyFile != NULL) { key.assign(parameters.certificateKeyFile); } - if (parameters.certificateKeyPassword) + if (parameters.certificateKeyPassword != NULL) { password.assign(parameters.certificateKeyPassword); } - client.SetClientCertificate(certificate, key, password); + client.SetClientCertificate(SystemToolbox::PathFromUtf8(certificate), SystemToolbox::PathFromUtf8(key), password); } client.SetPkcs11Enabled(parameters.pkcs11 ? true : false); @@ -5019,14 +5019,14 @@ { case _OrthancPluginService_GetOrthancPath: { - std::string s = SystemToolbox::GetPathToExecutable(); + std::string s = SystemToolbox::PathToUtf8(SystemToolbox::GetPathToExecutable()); *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s); return true; } case _OrthancPluginService_GetOrthancDirectory: { - std::string s = SystemToolbox::GetDirectoryOfExecutable(); + std::string s = SystemToolbox::PathToUtf8(SystemToolbox::GetDirectoryOfExecutable()); *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s); return true; } @@ -5927,7 +5927,7 @@ case _OrthancPluginService_KeysValuesIteratorGetKey: { const _OrthancPluginKeysValuesIteratorGetKey& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetKey*>(parameters); - StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + const StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<const StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); *p.target = iterator.GetKey().c_str(); return true; } @@ -5935,7 +5935,7 @@ case _OrthancPluginService_KeysValuesIteratorGetValue: { const _OrthancPluginKeysValuesIteratorGetValue& p = *reinterpret_cast<const _OrthancPluginKeysValuesIteratorGetValue*>(parameters); - StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); + const StatelessDatabaseOperations::KeysValuesIterator& iterator = *reinterpret_cast<const StatelessDatabaseOperations::KeysValuesIterator*>(p.iterator); CopyToMemoryBuffer(p.target, iterator.GetValue()); return true; } @@ -6740,7 +6740,7 @@ assert(reader_ != NULL); } - virtual ~HttpServerChunkedReader() + virtual ~HttpServerChunkedReader() ORTHANC_OVERRIDE { assert(reader_ != NULL); parameters_.finalize(reader_);
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h Wed Nov 12 19:06:12 2025 +0100 @@ -285,7 +285,7 @@ public: explicit OrthancPlugins(const std::string& databaseServerIdentifier); - virtual ~OrthancPlugins(); + virtual ~OrthancPlugins() ORTHANC_OVERRIDE; void SetServerContext(ServerContext& context);
--- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer32.h Wed Nov 12 19:06:12 2025 +0100 @@ -44,7 +44,7 @@ public: PluginMemoryBuffer32(); - virtual ~PluginMemoryBuffer32() + virtual ~PluginMemoryBuffer32() ORTHANC_OVERRIDE { Clear(); }
--- a/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/PluginMemoryBuffer64.h Wed Nov 12 19:06:12 2025 +0100 @@ -44,7 +44,7 @@ public: PluginMemoryBuffer64(); - virtual ~PluginMemoryBuffer64() + virtual ~PluginMemoryBuffer64() ORTHANC_OVERRIDE { Clear(); }
--- a/OrthancServer/Plugins/Engine/PluginsJob.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Engine/PluginsJob.h Wed Nov 12 19:06:12 2025 +0100 @@ -47,7 +47,7 @@ explicit PluginsJob(const _OrthancPluginCreateJob2& parameters); - virtual ~PluginsJob(); + virtual ~PluginsJob() ORTHANC_OVERRIDE; virtual void Start() ORTHANC_OVERRIDE {
--- a/OrthancServer/Plugins/Samples/AdoptDicomInstance/Plugin.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/AdoptDicomInstance/Plugin.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -79,9 +79,9 @@ buffer.Assign(info.toStyledString()); *customData = buffer.Release(); - const std::string path = GetStorageDirectoryPath(uuid); + const boost::filesystem::path path = GetStorageDirectoryPath(uuid); LOG(WARNING) << "Creating non-adopted file: " << path; - Orthanc::SystemToolbox::WriteFile(content, size, path); + Orthanc::SystemToolbox::WriteFile(content, size, path, false /* no fsync */); return OrthancPluginErrorCode_Success; } @@ -104,7 +104,7 @@ throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); } - std::string path; + boost::filesystem::path path; if (info["IsAdopted"].asBool()) { @@ -153,7 +153,7 @@ } else { - const std::string path = GetStorageDirectoryPath(uuid); + const boost::filesystem::path path = GetStorageDirectoryPath(uuid); LOG(WARNING) << "Removing non-adopted file from: " << path; Orthanc::SystemToolbox::RemoveFile(path); }
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c Wed Nov 12 19:06:12 2025 +0100 @@ -239,7 +239,7 @@ else { const char* studyId = request->groups[0]; - int32_t statusHasChanged = 0; + uint8_t statusHasChanged = 0; if (strcmp(request->groups[1], "stabilize") == 0) {
--- a/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -44,8 +44,9 @@ return OrthancPluginErrorCode_Success; } - std::string path = "/" + std::string(request->groups[0]); - std::string mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path)); + const std::string path = "/" + std::string(request->groups[0]); + std::string mime = Orthanc::EnumerationToString( + Orthanc::SystemToolbox::AutodetectMimeType(Orthanc::SystemToolbox::PathFromUtf8(path))); try {
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -26,6 +26,7 @@ #include "../../../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h" #include "../../../../OrthancFramework/Sources/Logging.h" #include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" #include "../Common/OrthancPluginCppWrapper.h" #include <boost/thread.hpp> @@ -329,9 +330,9 @@ std::string pathStorage = orthancConfig.GetStringValue("StorageDirectory", "OrthancStorage"); LOG(WARNING) << "DelayedDeletion - Path to the storage area: " << pathStorage; - storage_.reset(new Orthanc::FilesystemStorage(pathStorage)); + storage_.reset(new Orthanc::FilesystemStorage(Orthanc::SystemToolbox::PathFromUtf8(pathStorage))); - boost::filesystem::path defaultDbPath = boost::filesystem::path(pathStorage) / (std::string("pending-deletions.") + databaseServerIdentifier_ + ".db"); + boost::filesystem::path defaultDbPath = Orthanc::SystemToolbox::PathFromUtf8(pathStorage) / (std::string("pending-deletions.") + databaseServerIdentifier_ + ".db"); std::string dbPath = delayedDeletionConfig.GetStringValue("Path", defaultDbPath.string()); LOG(WARNING) << "DelayedDeletion - Path to the SQLite database: " << dbPath;
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -107,7 +107,7 @@ bool isInPeriod() const { time_t now = time(NULL); - tm* nowLocalTime = localtime(&now); + const tm* nowLocalTime = localtime(&now); if (nowLocalTime->tm_wday != weekday_) {
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt Wed Nov 12 19:06:12 2025 +0100 @@ -27,22 +27,31 @@ SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") +include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) -include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake) +set(ENABLE_JPEG ON CACHE INTERNAL "") +set(ENABLE_PNG ON CACHE INTERNAL "") +set(ENABLE_ZLIB ON CACHE INTERNAL "") +set(ENABLE_LOCALE ON CACHE INTERNAL "") +set(ENABLE_DCMTK ON CACHE INTERNAL "") +set(ENABLE_MODULE_DICOM ON CACHE INTERNAL "") +set(ENABLE_MODULE_JOBS OFF CACHE INTERNAL "") + +include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) add_library(ModalityWorklists SHARED Plugin.cpp ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp - ${JSONCPP_SOURCES} - ${BOOST_SOURCES} + ${ORTHANC_CORE_SOURCES} + ${ORTHANC_DICOM_SOURCES} + ${AUTOGENERATED_SOURCES} ) DefineSourceBasenameForTarget(ModalityWorklists) +target_link_libraries(ModalityWorklists ${DCMTK_LIBRARIES}) + message("Setting the version of the plugin to ${MODALITY_WORKLISTS_VERSION}") add_definitions( -DMODALITY_WORKLISTS_VERSION="${MODALITY_WORKLISTS_VERSION}"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/OrthancFrameworkDependencies.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -0,0 +1,102 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * Remove the dependency upon ICU in plugins, as this greatly increase + * the size of the resulting binaries, since they must embed the ICU + * dictionary. + **/ + +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + +#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" +#include "../../../../OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp" +#include "../../../../OrthancFramework/Sources/Compression/GzipCompressor.cpp" +#include "../../../../OrthancFramework/Sources/Compression/ZlibCompressor.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomElement.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomTag.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomValue.cpp" +#include "../../../../OrthancFramework/Sources/DicomFormat/Window.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp" +#include "../../../../OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp" +#include "../../../../OrthancFramework/Sources/Enumerations.cpp" +#include "../../../../OrthancFramework/Sources/HttpServer/HttpOutput.cpp" +#include "../../../../OrthancFramework/Sources/Images/IImageWriter.cpp" +#include "../../../../OrthancFramework/Sources/Images/Image.cpp" +#include "../../../../OrthancFramework/Sources/Images/ImageAccessor.cpp" +#include "../../../../OrthancFramework/Sources/Images/ImageBuffer.cpp" +#include "../../../../OrthancFramework/Sources/Images/ImageProcessing.cpp" +#include "../../../../OrthancFramework/Sources/Images/JpegErrorManager.cpp" +#include "../../../../OrthancFramework/Sources/Images/JpegReader.cpp" +#include "../../../../OrthancFramework/Sources/Images/JpegWriter.cpp" +#include "../../../../OrthancFramework/Sources/Images/PamReader.cpp" +#include "../../../../OrthancFramework/Sources/Images/PamWriter.cpp" +#include "../../../../OrthancFramework/Sources/Images/PngReader.cpp" +#include "../../../../OrthancFramework/Sources/Images/PngWriter.cpp" +#include "../../../../OrthancFramework/Sources/Logging.cpp" +#include "../../../../OrthancFramework/Sources/MetricsRegistry.cpp" +#include "../../../../OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp" +#include "../../../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp" +#include "../../../../OrthancFramework/Sources/OrthancException.cpp" +#include "../../../../OrthancFramework/Sources/OrthancFramework.cpp" +#include "../../../../OrthancFramework/Sources/RestApi/RestApiOutput.cpp" +#include "../../../../OrthancFramework/Sources/SerializationToolbox.cpp" +#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp" +#include "../../../../OrthancFramework/Sources/TemporaryFile.cpp" +#include "../../../../OrthancFramework/Sources/Toolbox.cpp" + +namespace Orthanc +{ + void HttpClient::GlobalInitialize() + { + } + + void HttpClient::GlobalFinalize() + { + } +}
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -24,17 +24,84 @@ #define MODALITY_WORKLISTS_NAME "worklists" #include "../../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../../OrthancFramework/Sources/Logging.h" +#include "../../../../OrthancFramework/Sources/Toolbox.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" +#include "../../../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomPath.h" +#include "../../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h" #include "../Common/OrthancPluginCppWrapper.h" +#include <boost/thread.hpp> #include <boost/filesystem.hpp> #include <json/value.h> #include <string.h> #include <iostream> #include <algorithm> -static std::string folder_; +static boost::filesystem::path worklistDirectory_; static bool filterIssuerAet_ = false; static unsigned int limitAnswers_ = 0; +static std::unique_ptr<boost::thread> worklistHousekeeperThread_; +static bool worklistHousekeeperThreadShouldStop_ = false; +static bool deleteWorklistsOnStableStudy_ = true; +static unsigned int hkIntervalInSeconds_ = 60; +static unsigned int deleteDelayInHours_ = 24; +static std::unique_ptr<OrthancPlugins::KeyValueStore> worklistsStore_; +static bool setStudyInstanceUidIfMissing_ = true; + +enum WorklistStorageType +{ + WorklistStorageType_Folder = 1, + WorklistStorageType_OrthancDb = 2 +}; + +static WorklistStorageType worklistStorage_ = WorklistStorageType_Folder; + +struct Worklist +{ + std::string id_; + std::string createdAt_; + std::string dicomContent_; + + Worklist(const std::string& id, + const std::string& dicomContent) : + id_(id), + createdAt_(Orthanc::SystemToolbox::GetNowIsoString(true)), + dicomContent_(dicomContent) + { + } + + + Worklist(const std::string& id, const Json::Value& jsonWl) : + id_(id), + createdAt_(jsonWl["CreatedAt"].asString()) + { + std::string b64DicomContent = jsonWl["Dicom"].asString(); + Orthanc::Toolbox::DecodeBase64(dicomContent_, b64DicomContent); + } + + void Serialize(std::string& target) const + { + Json::Value t; + t["CreatedAt"] = createdAt_; + std::string b64DicomContent; + Orthanc::Toolbox::EncodeBase64(b64DicomContent, dicomContent_); + t["Dicom"] = b64DicomContent; + + Orthanc::Toolbox::WriteFastJson(target, t); + } + + bool IsOlderThan(unsigned int delayInHours) const + { + boost::posix_time::ptime now(boost::posix_time::from_iso_string(Orthanc::SystemToolbox::GetNowIsoString(true))); + boost::posix_time::ptime creationDate(boost::posix_time::from_iso_string(createdAt_)); + + return (now - creationDate).total_seconds() > (3600 * deleteDelayInHours_); + } +}; /** * This is the main function for matching a DICOM worklist against a query. @@ -42,10 +109,10 @@ static bool MatchWorklist(OrthancPluginWorklistAnswers* answers, const OrthancPluginWorklistQuery* query, const OrthancPlugins::FindMatcher& matcher, - const std::string& path) + const std::string& dicomContent) { OrthancPlugins::MemoryBuffer dicom; - dicom.ReadFile(path); + dicom.Assign(dicomContent); if (matcher.IsMatch(dicom)) { @@ -137,70 +204,116 @@ } } +static void ListWorklistsFromDb(std::vector<Worklist>& target) +{ + assert(worklistStorage_ == WorklistStorageType_OrthancDb); + assert(worklistsStore_); + + std::unique_ptr<OrthancPlugins::KeyValueStore::Iterator> it(worklistsStore_->CreateIterator()); + + while (it->Next()) + { + std::string serialized; + it->GetValue(serialized); + + Json::Value jsonWl; + if (Orthanc::Toolbox::ReadJson(jsonWl, serialized)) + { + Worklist wl(it->GetKey(), jsonWl); + target.push_back(wl); + } + }; + +} -OrthancPluginErrorCode Callback(OrthancPluginWorklistAnswers* answers, - const OrthancPluginWorklistQuery* query, - const char* issuerAet, - const char* calledAet) +static void ListWorklistsFromFolder(std::vector<Worklist>& target) +{ + assert(worklistStorage_ == WorklistStorageType_Folder); + + // Loop over the regular files in the database folder + namespace fs = boost::filesystem; + + fs::path source = worklistDirectory_; + fs::directory_iterator end; + + try + { + for (fs::directory_iterator it(source); it != end; ++it) + { + fs::file_type type(it->status().type()); + + if (type == fs::regular_file || + type == fs::reparse_file) // cf. BitBucket issue #11 + { + std::string extension = it->path().extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), tolower); // Convert to lowercase + + if (extension == ".wl") + { + std::string worklistId = Orthanc::SystemToolbox::PathToUtf8(it->path().filename().replace_extension("")); + std::string dicomContent; + Orthanc::SystemToolbox::ReadFile(dicomContent, it->path()); + + Worklist wl(worklistId, dicomContent); + target.push_back(wl); + } + } + } + } + catch (fs::filesystem_error&) + { + LOG(ERROR) << "Inexistent folder while scanning for worklists: " + source.string(); + } +} + + +OrthancPluginErrorCode WorklistCallback(OrthancPluginWorklistAnswers* answers, + const OrthancPluginWorklistQuery* query, + const char* issuerAet, + const char* calledAet) { try { + unsigned int parsedFilesCount = 0; + unsigned int matchedWorklistCount = 0; + // Construct an object to match the worklists in the database against the C-Find query std::unique_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet)); - // Loop over the regular files in the database folder - namespace fs = boost::filesystem; + std::vector<Worklist> worklists; - fs::path source(folder_); - fs::directory_iterator end; - - try + if (worklistStorage_ == WorklistStorageType_Folder) { - unsigned int parsedFilesCount = 0; - unsigned int matchedWorklistCount = 0; - - for (fs::directory_iterator it(source); it != end; ++it) - { - fs::file_type type(it->status().type()); - - if (type == fs::regular_file || - type == fs::reparse_file) // cf. BitBucket issue #11 - { - std::string extension = it->path().extension().string(); - std::transform(extension.begin(), extension.end(), extension.begin(), tolower); // Convert to lowercase + ListWorklistsFromFolder(worklists); + } + else if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + ListWorklistsFromDb(worklists); + } - if (extension == ".wl") - { - parsedFilesCount++; - // We found a worklist (i.e. a DICOM find with extension ".wl"), match it against the query - if (MatchWorklist(answers, query, *matcher, it->path().string())) - { - if (limitAnswers_ != 0 && - matchedWorklistCount >= limitAnswers_) - { - // Too many answers are to be returned wrt. the - // "LimitAnswers" configuration parameter. Mark the - // C-FIND result as incomplete. - OrthancPluginWorklistMarkIncomplete(OrthancPlugins::GetGlobalContext(), answers); - return OrthancPluginErrorCode_Success; - } - - ORTHANC_PLUGINS_LOG_INFO("Worklist matched: " + it->path().string()); - matchedWorklistCount++; - } - } + for (std::vector<Worklist>::const_iterator it = worklists.begin(); it != worklists.end(); ++it) + { + if (MatchWorklist(answers, query, *matcher, it->dicomContent_)) + { + if (limitAnswers_ != 0 && + matchedWorklistCount >= limitAnswers_) + { + // Too many answers are to be returned wrt. the + // "LimitAnswers" configuration parameter. Mark the + // C-FIND result as incomplete. + OrthancPluginWorklistMarkIncomplete(OrthancPlugins::GetGlobalContext(), answers); + return OrthancPluginErrorCode_Success; } + + LOG(INFO) << "Worklist matched: " << it->id_; + matchedWorklistCount++; } - - ORTHANC_PLUGINS_LOG_INFO("Worklist C-Find: parsed " + boost::lexical_cast<std::string>(parsedFilesCount) + - " files, found " + boost::lexical_cast<std::string>(matchedWorklistCount) + " match(es)"); } - catch (fs::filesystem_error&) - { - ORTHANC_PLUGINS_LOG_ERROR("Inexistent folder while scanning for worklists: " + source.string()); - return OrthancPluginErrorCode_DirectoryExpected; - } + + LOG(INFO) << "Worklist C-Find: parsed " << boost::lexical_cast<std::string>(parsedFilesCount) << + " worklists, found " << boost::lexical_cast<std::string>(matchedWorklistCount) << " match(es)"; + return OrthancPluginErrorCode_Success; } @@ -210,13 +323,411 @@ } } +static void DeleteWorklist(const std::string& worklistId) +{ + switch (worklistStorage_) + { + case WorklistStorageType_Folder: + { + boost::filesystem::path path = worklistDirectory_ / (worklistId + ".wl"); + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + Orthanc::SystemToolbox::RemoveFile(path); + break; + } + + case WorklistStorageType_OrthancDb: + if (worklistsStore_.get() != NULL) + { + std::string notUsed; + if (!worklistsStore_->GetValue(notUsed, worklistId)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); + } + worklistsStore_->DeleteKey(worklistId); + } + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + +} + +static void WorklistHkWorkerThread() +{ + OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "WL HOUSEKEEPER"); + + OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Worklist Housekeeper worker thread"); + Orthanc::Toolbox::ElapsedTimer timer; + + while (!worklistHousekeeperThreadShouldStop_) + { + boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + + if (timer.GetElapsedMilliseconds() > (hkIntervalInSeconds_ * 1000)) + { + timer.Restart(); + LOG(INFO) << "Performing Worklist Housekeeping"; + + std::vector<Worklist> worklists; + + if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + ListWorklistsFromDb(worklists); + } + else if (worklistStorage_ == WorklistStorageType_Folder) + { + ListWorklistsFromFolder(worklists); + } + + for (std::vector<Worklist>::const_iterator it = worklists.begin(); it != worklists.end(); ++it) + { + if (deleteDelayInHours_ > 0 && it->IsOlderThan(deleteDelayInHours_)) + { + LOG(INFO) << "Deleting worklist " << it->id_ << " " << deleteDelayInHours_ << " hours after its creation"; + DeleteWorklist(it->id_); + } + else if (deleteWorklistsOnStableStudy_) + { + std::string studyInstanceUid; + std::string patientId; + + Orthanc::ParsedDicomFile parsed(it->dicomContent_); + + if (parsed.GetTagValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID) && + parsed.GetTagValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID)) + { + Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, "fake-id", "fake-id"); + const std::string& studyOrthancId = hasher.HashStudy(); + + Json::Value studyInfo; + if (OrthancPlugins::RestApiGet(studyInfo, "/studies/" + studyOrthancId, false)) + { + if (studyInfo["IsStable"].asBool()) + { + LOG(INFO) << "Deleting worklist " << it->id_ << " because its study is now stable"; + DeleteWorklist(it->id_); + } + } + } + } + } + } + } +} + + +static Orthanc::DicomToJsonFormat GetFormat(const OrthancPluginHttpRequest* request) +{ + std::map<std::string, std::string> getArguments; + OrthancPlugins::GetGetArguments(getArguments, request); + + Orthanc::DicomToJsonFormat format = Orthanc::DicomToJsonFormat_Human; + + if (getArguments.find("format") != getArguments.end()) + { + format = Orthanc::StringToDicomToJsonFormat(getArguments["format"]); + } + + return format; +} + +static Orthanc::ParsedDicomFile* GetWorklist(const std::string& id) +{ + std::string fileContent; + + switch (worklistStorage_) + { + case WorklistStorageType_Folder: + { + boost::filesystem::path path = worklistDirectory_ / Orthanc::SystemToolbox::PathFromUtf8(id + ".wl"); // the id might be a filename from a file that was pushed by an external program (therefore, it can contain fancy characters) + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Worklist not found"); + } + + Orthanc::SystemToolbox::ReadFile(fileContent, path); + break; + } + + case WorklistStorageType_OrthancDb: + { + std::string serializedWl; + if (!worklistsStore_->GetValue(serializedWl, id)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource, "Worklist not found"); + } + + Json::Value jsonWl; + if (Orthanc::Toolbox::ReadJson(jsonWl, serializedWl)) + { + Worklist wl(id, jsonWl); + fileContent = wl.dicomContent_; + } + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + return new Orthanc::ParsedDicomFile(fileContent); +} + +static void SerializeWorklistForApi(Json::Value& target, const std::string& id, const Orthanc::ParsedDicomFile& parsed, Orthanc::DicomToJsonFormat format) +{ + target["ID"] = id; + target["Tags"] = Json::objectValue; + parsed.DatasetToJson(target["Tags"], format, Orthanc::DicomToJsonFlags_None, 0); +} extern "C" { + + OrthancPluginErrorCode ListWorklists(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPlugins::AnswerMethodNotAllowed(output, "GET"); + } + else + { + Json::Value response = Json::arrayValue; + Orthanc::DicomToJsonFormat format = GetFormat(request); + + std::vector<Worklist> worklists; + + if (worklistStorage_ == WorklistStorageType_Folder) + { + ListWorklistsFromFolder(worklists); + } + else if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + ListWorklistsFromDb(worklists); + } + + for (std::vector<Worklist>::const_iterator it = worklists.begin(); it != worklists.end(); ++it) + { + Orthanc::ParsedDicomFile parsed(it->dicomContent_); + Json::Value jsonWl; + SerializeWorklistForApi(jsonWl, it->id_, parsed, format); + + if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + jsonWl["CreationDate"] = it->createdAt_; + } + + response.append(jsonWl); + } + + OrthancPlugins::AnswerJson(response, output); + } + + return OrthancPluginErrorCode_Success; + } + + + void CreateOrUpdateWorklist(std::string& worklistId, + bool defaultForceValue, + OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + Json::Value body; + + if (!OrthancPlugins::ReadJson(body, request->body, request->bodySize)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected"); + } + + if (!body.isMember("Tags") || !body["Tags"].isObject()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'Tags' field is missing or not a JSON object"); + } + + bool force = defaultForceValue; + if (body.isMember("Force")) { + force = body["Force"].asBool(); + } + + Json::Value& jsonWorklist = body["Tags"]; + + if (!jsonWorklist.isMember("SpecificCharacterSet")) + { + jsonWorklist["SpecificCharacterSet"] = Orthanc::GetDicomSpecificCharacterSet(Orthanc::Encoding_Utf8); + } + + std::unique_ptr<Orthanc::ParsedDicomFile> dicom(Orthanc::ParsedDicomFile::CreateFromJson(jsonWorklist, Orthanc::DicomFromJsonFlags_None, "")); + + if (!force) + { + if (!dicom->HasTag(Orthanc::DICOM_TAG_SCHEDULED_PROCEDURE_STEP_SEQUENCE)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'Tags' is missing a 'ScheduledProcedureStepSequence'. Use 'Force': true to bypass this check."); + } + Orthanc::DicomMap step; + if (!dicom->LookupSequenceItem(step, Orthanc::DicomPath::Parse("ScheduledProcedureStepSequence"), 0) || !step.HasTag(Orthanc::DICOM_TAG_MODALITY)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'ScheduledProcedureStepSequence' is missing a 'Modality' Use 'Force': true to bypass this check."); + } + if (!dicom->LookupSequenceItem(step, Orthanc::DicomPath::Parse("ScheduledProcedureStepSequence"), 0) || !step.HasTag(Orthanc::DICOM_TAG_SCHEDULED_PROCEDURE_STEP_START_DATE)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "'ScheduledProcedureStepSequence' is missing a 'ScheduledProcedureStepStartDate' Use 'Force': true to bypass this check."); + } + } + + dicom->SetIfAbsent(Orthanc::DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID, "1.2.276.0.7230010.3.1.0.1"); + + if (setStudyInstanceUidIfMissing_) + { + dicom->SetIfAbsent(Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, Orthanc::FromDcmtkBridge::GenerateUniqueIdentifier(Orthanc::ResourceType_Study)); + } + + if (worklistId.empty()) + { + worklistId = Orthanc::Toolbox::GenerateUuid(); + } + + std::string dicomContent; + dicom->SaveToMemoryBuffer(dicomContent); + + switch (worklistStorage_) + { + case WorklistStorageType_Folder: + Orthanc::SystemToolbox::WriteFile(dicomContent.empty() ? NULL : dicomContent.c_str(), dicomContent.size(), + worklistDirectory_ / Orthanc::SystemToolbox::PathFromUtf8(worklistId + ".wl"), true); + break; + + case WorklistStorageType_OrthancDb: + { + Worklist wl(worklistId, dicomContent); + std::string serializedWl; + wl.Serialize(serializedWl); + + worklistsStore_->Store(worklistId, serializedWl); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + Json::Value response; + + response["ID"] = worklistId; + response["Path"] = "/worklists/" + worklistId; + + OrthancPlugins::AnswerJson(response, output); + } + + + OrthancPluginErrorCode GetPutDeleteWorklist(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + std::string worklistId = std::string(request->groups[0]); + + if (request->method == OrthancPluginHttpMethod_Delete) + { + DeleteWorklist(worklistId); + + OrthancPlugins::AnswerString("{}", "application/json", output); + } + else if (request->method == OrthancPluginHttpMethod_Get) + { + Orthanc::DicomToJsonFormat format = GetFormat(request); + std::unique_ptr<Orthanc::ParsedDicomFile> parsed(GetWorklist(worklistId)); + + Json::Value jsonWl; + SerializeWorklistForApi(jsonWl, worklistId, *parsed, format); + + OrthancPlugins::AnswerJson(jsonWl, output); + } + else if (request->method == OrthancPluginHttpMethod_Put) + { + CreateOrUpdateWorklist(worklistId, true, output, url, request); + } + else + { + OrthancPlugins::AnswerMethodNotAllowed(output, "DELETE,GET"); + } + + return OrthancPluginErrorCode_Success; + } + + + OrthancPluginErrorCode PostCreateWorklist(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) + { + if (request->method != OrthancPluginHttpMethod_Post) + { + OrthancPlugins::AnswerMethodNotAllowed(output, "POST"); + } + else + { + std::string worklistId; + CreateOrUpdateWorklist(worklistId, false, output, url, request); + } + return OrthancPluginErrorCode_Success; + } + + static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, + OrthancPluginResourceType resourceType, + const char *resourceId) + { + try + { + if (changeType == OrthancPluginChangeType_OrthancStarted) + { + Json::Value system; + bool hasKeyValueStores = (OrthancPlugins::RestApiGet(system, "/system", false) && system.isMember("Capabilities") && + system["Capabilities"].isMember("HasKeyValueStores") && system["Capabilities"]["HasKeyValueStores"].asBool()); + + if (worklistStorage_ == WorklistStorageType_OrthancDb && !hasKeyValueStores) + { + LOG(ERROR) << "The Orthanc DB plugin does not support Key Value Stores. It is therefore impossible to store the worklists in Orthanc Database"; + return OrthancPluginErrorCode_IncompatibleConfigurations; + } + + if (deleteDelayInHours_ > 0 && !hasKeyValueStores) + { + LOG(ERROR) << "The Orthanc DB plugin does not support Key Value Stores. It is therefore impossible to use the \"DeleteWorklistsDelay\" option"; + return OrthancPluginErrorCode_IncompatibleConfigurations; + } + + if (worklistStorage_ == WorklistStorageType_OrthancDb) + { + worklistsStore_.reset(new OrthancPlugins::KeyValueStore("worklists")); + } + + if (deleteDelayInHours_ > 0 || deleteWorklistsOnStableStudy_) + { + worklistHousekeeperThread_.reset(new boost::thread(WorklistHkWorkerThread)); + } + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception: " << e.What(); + } + catch (...) + { + LOG(ERROR) << "Uncatched native exception"; + } + return OrthancPluginErrorCode_Success; + } + ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c) { OrthancPlugins::SetGlobalContext(c, MODALITY_WORKLISTS_NAME); - + Orthanc::Logging::InitializePluginContext(c, MODALITY_WORKLISTS_NAME); + /* Check the version of the Orthanc core */ if (OrthancPluginCheckVersion(c) == 0) { @@ -226,6 +737,29 @@ return -1; } + { // init the OrthancFramework + static const char* const LOCALE = "Locale"; + static const char* const DEFAULT_ENCODING = "DefaultEncoding"; + + /** + * This function is a simplified version of function + * "Orthanc::OrthancInitialize()" that is executed when starting the + * Orthanc server. + **/ + OrthancPlugins::OrthancConfiguration globalConfig; + Orthanc::InitializeFramework(globalConfig.GetStringValue(LOCALE, ""), false /* loadPrivateDictionary */); + + std::string encoding; + if (globalConfig.LookupStringValue(encoding, DEFAULT_ENCODING)) + { + Orthanc::SetDefaultDicomEncoding(Orthanc::StringToEncoding(encoding.c_str())); + } + else + { + Orthanc::SetDefaultDicomEncoding(Orthanc::ORTHANC_DEFAULT_DICOM_ENCODING); + } + } + ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is initializing"); OrthancPluginSetDescription2(c, MODALITY_WORKLISTS_NAME, "Serve DICOM modality worklists from a folder with Orthanc."); @@ -237,19 +771,52 @@ bool enabled = worklists.GetBooleanValue("Enable", false); if (enabled) { - if (worklists.LookupStringValue(folder_, "Database")) + std::string folder; + if (worklists.LookupStringValue(folder, "Database") || worklists.LookupStringValue(folder, "Directory")) { - ORTHANC_PLUGINS_LOG_WARNING("The database of worklists will be read from folder: " + folder_); - OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback); + if (worklists.GetBooleanValue("SaveInOrthancDatabase", false)) + { + LOG(ERROR) << "Worklists plugin: you can not set the \"SaveInOrthancDatabase\" configuration to \"true\" once you have configured the \"Directory\" (or former \"Database\") configuration."; + return -1; + } + + worklistStorage_ = WorklistStorageType_Folder; + worklistDirectory_ = folder; //Orthanc::SystemToolbox::PathFromUtf8(folder); + LOG(WARNING) << "The database of worklists will be read from folder: " << folder; + } + else if (worklists.GetBooleanValue("SaveInOrthancDatabase", false)) + { + + worklistStorage_ = WorklistStorageType_OrthancDb; + ORTHANC_PLUGINS_LOG_WARNING("The database of worklists will be read from Orthanc Database"); } else { - ORTHANC_PLUGINS_LOG_ERROR("The configuration option \"Worklists.Database\" must contain a path"); + LOG(ERROR) << "The configuration option \"Worklists.Directory\" must contain a path"; return -1; } + OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), WorklistCallback); + filterIssuerAet_ = worklists.GetBooleanValue("FilterIssuerAet", false); limitAnswers_ = worklists.GetUnsignedIntegerValue("LimitAnswers", 0); + + deleteWorklistsOnStableStudy_ = worklists.GetBooleanValue("DeleteWorklistsOnStableStudy", true); + hkIntervalInSeconds_ = worklists.GetUnsignedIntegerValue("HousekeepingInterval", 60); + deleteDelayInHours_ = worklists.GetUnsignedIntegerValue("DeleteWorklistsDelay", 0); + setStudyInstanceUidIfMissing_ = worklists.GetBooleanValue("SetStudyInstanceUidIfMissing", true); + + if (deleteDelayInHours_ > 0 && worklistStorage_ == WorklistStorageType_Folder) + { + LOG(ERROR) << "Worklists plugin: you can not set the \"DeleteWorklistsDelay\" configuration once you have configured the \"Directory\" (or former \"Database\") configuration. This feature only works once \"SaveInOrthancDatabase\" is set to true."; + return -1; + } + + OrthancPluginRegisterOnChangeCallback(OrthancPlugins::GetGlobalContext(), OnChangeCallback); + + OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists/create", PostCreateWorklist); + OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists/([^/]+)", GetPutDeleteWorklist); + OrthancPluginRegisterRestCallback(OrthancPlugins::GetGlobalContext(), "/worklists", ListWorklists); } else { @@ -263,6 +830,13 @@ ORTHANC_PLUGINS_API void OrthancPluginFinalize() { ORTHANC_PLUGINS_LOG_WARNING("Sample worklist plugin is finalizing"); + + worklistHousekeeperThreadShouldStop_ = true; + if (worklistHousekeeperThread_.get() != NULL && worklistHousekeeperThread_->joinable()) + { + worklistHousekeeperThread_->join(); + } + worklistHousekeeperThread_.reset(NULL); }
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -36,7 +36,9 @@ uint16_t StoreRequestHandler::Handle(DcmDataset& dicom, const std::string& remoteIp, const std::string& remoteAet, - const std::string& calledAet) + const std::string& calledAet, + uint16_t originatorMessageId, + const std::string& originatorAet) { std::string buffer; std::string errorMessage;
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h Wed Nov 12 19:06:12 2025 +0100 @@ -44,5 +44,7 @@ virtual uint16_t Handle(DcmDataset& dicom, const std::string& remoteIp, const std::string& remoteAet, - const std::string& calledAet) ORTHANC_OVERRIDE; + const std::string& calledAet, + uint16_t originatorMessageId, + const std::string& originatorAet) ORTHANC_OVERRIDE; };
--- a/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt Wed Nov 12 19:06:12 2025 +0100 @@ -27,18 +27,24 @@ SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)") SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages") -SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp") -SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost") +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake) -include(${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Resources/CMake/BoostConfiguration.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../Common/OrthancPluginsExports.cmake) + +include_directories( + ${CMAKE_SOURCE_DIR}/../../Include/ + ) add_library(ServeFolders SHARED Plugin.cpp + ${ORTHANC_CORE_SOURCES_DEPENDENCIES} + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Enumerations.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Logging.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/OrthancException.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/SystemToolbox.cpp + ${CMAKE_SOURCE_DIR}/../../../../OrthancFramework/Sources/Toolbox.cpp ${CMAKE_SOURCE_DIR}/../Common/OrthancPluginCppWrapper.cpp - ${JSONCPP_SOURCES} - ${BOOST_SOURCES} ) DefineSourceBasenameForTarget(ServeFolders)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Plugins/Samples/ServeFolders/OrthancFrameworkDependencies.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -0,0 +1,54 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2025 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +/** + * Remove the dependency upon ICU in plugins, as this greatly increase + * the size of the resulting binaries, since they must embed the ICU + * dictionary. + **/ + +#if BOOST_LOCALE_WITH_ICU == 1 +# undef BOOST_LOCALE_WITH_ICU +# if ORTHANC_STATIC_ICU == 1 +# include <unicode/udata.h> + +// Define an empty ICU dictionary for static builds +extern "C" +{ + struct + { + double bogus; + uint8_t *bytes; + } U_ICUDATA_ENTRY_POINT = { 0.0, NULL }; +} + +# endif +#endif + + +#include "../../../../OrthancFramework/Sources/ChunkedBuffer.cpp" +#include "../../../../OrthancFramework/Sources/Enumerations.cpp" +#include "../../../../OrthancFramework/Sources/Logging.cpp" +#include "../../../../OrthancFramework/Sources/OrthancException.cpp" +#include "../../../../OrthancFramework/Sources/SystemToolbox.cpp" +#include "../../../../OrthancFramework/Sources/Toolbox.cpp"
--- a/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -25,6 +25,8 @@ #include "../Common/OrthancPluginCppWrapper.h" +#include "../../../../OrthancFramework/Sources/SystemToolbox.h" + #include <json/value.h> #include <boost/filesystem.hpp> #include <boost/date_time/posix_time/posix_time.hpp> @@ -158,11 +160,11 @@ if (LookupFolder(folder, output, request)) { const fs::path item(request->groups[1]); - const fs::path parent((fs::path(folder) / item).parent_path()); + const fs::path parent((Orthanc::SystemToolbox::PathFromUtf8(folder) / item).parent_path()); if (item.filename().string() == "index.html" && fs::is_directory(parent) && - !fs::is_regular_file(fs::path(folder) / item)) + !fs::is_regular_file(Orthanc::SystemToolbox::PathFromUtf8(folder) / item)) { // On-the-fly generation of an "index.html" std::string s; @@ -216,7 +218,7 @@ } boost::posix_time::ptime lastModification = - boost::posix_time::from_time_t(fs::last_write_time(path)); + boost::posix_time::from_time_t(fs::last_write_time(Orthanc::SystemToolbox::PathFromUtf8(path))); std::string t = boost::posix_time::to_iso_string(lastModification); OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Last-Modified", t.c_str());
--- a/OrthancServer/Resources/RunCppCheck.sh Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Resources/RunCppCheck.sh Wed Nov 12 19:06:12 2025 +0100 @@ -24,11 +24,8 @@ syntaxError:../../OrthancFramework/Sources/SQLite/FunctionContext.h:53 syntaxError:../../OrthancFramework/UnitTestsSources/DicomMapTests.cpp:74 syntaxError:../../OrthancFramework/UnitTestsSources/ZipTests.cpp:133 -syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:322 -uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:417 -unreadVariable:../../OrthancFramework/Sources/FileStorage/StorageAccessor.cpp -unreadVariable:../../OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp:1173 -unusedFunction +syntaxError:../../OrthancServer/UnitTestsSources/UnitTestsMain.cpp:325 +uninitMemberVar:../../OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp:419 useInitializationList:../../OrthancFramework/Sources/Images/PngReader.cpp:91 useInitializationList:../../OrthancFramework/Sources/Images/PngWriter.cpp:99 useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275 @@ -39,9 +36,46 @@ assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3066 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286 assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454 +variableScope:../../OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp:228 +variableScope:../../OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp:94 +uselessOverride:../../OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h:35 +uselessOverride:../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h:76 +cstyleCast:../../OrthancServer/Plugins/Engine/PluginsManager.cpp:85 +cstyleCast:../../OrthancServer/Plugins/Engine/PluginsManager.cpp:108 +cstyleCast:../../OrthancServer/Plugins/Engine/PluginsManager.cpp:124 +cstyleCast:../../OrthancServer/Plugins/Engine/PluginsManager.cpp:140 +constParameterPointer:../../OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h +constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:447 +constParameterPointer:../../OrthancFramework/Sources/Logging.cpp:451 +constParameterPointer:../../OrthancFramework/Sources/Toolbox.cpp:3053 +knownConditionTrueFalse:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:114 +knownConditionTrueFalse:../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp:425 +knownConditionTrueFalse:../../OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp:345 +throwInNoexceptFunction:../../OrthancFramework/Sources/Cache/MemoryStringCache.cpp:121 +throwInNoexceptFunction:../../OrthancFramework/Sources/Cache/MemoryCache.cpp:91 +throwInNoexceptFunction:../../OrthancFramework/Sources/Cache/MemoryObjectCache.cpp:99 +throwInNoexceptFunction:../../OrthancFramework/Sources/MallocMemoryBuffer.h:50 +throwInNoexceptFunction:../../OrthancFramework/Sources/MetricsRegistry.cpp:620 +throwInNoexceptFunction:../../OrthancFramework/Sources/MetricsRegistry.cpp:632 +throwInNoexceptFunction:../../OrthancFramework/Sources/MetricsRegistry.cpp:676 +throwInNoexceptFunction:../../OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp:1296 +throwInNoexceptFunction:../../OrthancServer/Sources/LuaScripting.cpp:830 +throwInNoexceptFunction:../../OrthancServer/Sources/StorageCommitmentReports.cpp:187 +throwInNoexceptFunction:../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h:218 +throwInNoexceptFunction:../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h:376 +throwInNoexceptFunction:../../OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h:496 +rethrowNoCurrentException:../../OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp +rethrowNoCurrentException:../../OrthancFramework/UnitTestsSources/RestApiTests.cpp EOF -${CPPCHECK} --enable=all --quiet --std=c++11 \ +# TODO: re-enable nullPointerOutOfMemory + +${CPPCHECK} -j 8 --enable=all --quiet --std=c++11 \ + --suppress=missingIncludeSystem \ + --suppress=missingInclude \ + --suppress=useStlAlgorithm \ + --suppress=nullPointerOutOfMemory \ + --check-level=exhaustive \ --suppressions-list=/tmp/cppcheck-suppressions.txt \ -DBOOST_HAS_DATE_TIME=1 \ -DBOOST_HAS_FILESYSTEM_V3=1 \
--- a/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -63,7 +63,7 @@ if (arguments.size() != 2 && arguments.size() != 3) { fprintf(stderr, "Maintenance tool to recover a DICOM file that was compressed by Orthanc.\n\n"); - fprintf(stderr, "Usage: %s <input> [output]\n", Orthanc::SystemToolbox::PathToUtf8(arguments[0]).c_str()); + fprintf(stderr, "Usage: %s <input> [output]\n", arguments[0].c_str()); fprintf(stderr, "If \"output\" is not given, the data will be output to stdout\n"); return -1; }
--- a/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -187,7 +187,6 @@ request.GetOrthancIdentifiers(), ResourceType_Series, request.GetLevel()); } else if (request.GetMetadataConstraintsCount() == 0 && - request.GetOrdering().empty() && !request.GetOrthancIdentifiers().HasPatientId() && !request.GetOrthancIdentifiers().HasStudyId() && !request.GetOrthancIdentifiers().HasSeriesId() &&
--- a/OrthancServer/Sources/Database/PrepareDatabase.sql Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/Database/PrepareDatabase.sql Wed Nov 12 19:06:12 2025 +0100 @@ -165,4 +165,4 @@ -- Set the version of the database schema -- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration -INSERT INTO GlobalProperties VALUES (1, "6"); +INSERT INTO GlobalProperties VALUES (1, 6);
--- a/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -67,7 +67,7 @@ } std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size()); - MimeType contentType = SystemToolbox::AutodetectMimeType(resourcePath); + MimeType contentType = SystemToolbox::AutodetectMimeType(Orthanc::SystemToolbox::PathFromUtf8(resourcePath)); try {
--- a/OrthancServer/Sources/LuaScripting.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/LuaScripting.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -1034,7 +1034,7 @@ return true; } - bool LuaScripting::FilterIncomingCStoreInstance(uint16_t& dimseStatus, + bool LuaScripting::FilterIncomingCStoreInstance(uint16_t& /*dimseStatus*/, const DicomInstanceToStore& instance, const Json::Value& simplified) {
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -82,8 +82,7 @@ } - bool OrthancFindRequestHandler::FilterQueryTag(std::string& value /* can be modified */, - ResourceType level, + bool OrthancFindRequestHandler::FilterQueryTag(ResourceType level, const DicomTag& tag, ModalityManufacturer manufacturer) { @@ -417,7 +416,7 @@ continue; } - if (FilterQueryTag(value, level, tag, manufacturer)) + if (FilterQueryTag(level, tag, manufacturer)) { ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
--- a/OrthancServer/Sources/OrthancFindRequestHandler.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancFindRequestHandler.h Wed Nov 12 19:06:12 2025 +0100 @@ -40,8 +40,7 @@ bool HasReachedLimit(const DicomFindAnswers& answers, ResourceType level) const; - bool FilterQueryTag(std::string& value /* can be modified */, - ResourceType level, + bool FilterQueryTag(ResourceType level, const DicomTag& tag, ModalityManufacturer manufacturer);
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -133,13 +133,14 @@ LST_HEAD **l = &assoc->params->DULparams.acceptedPresentationContext; if (*l != NULL) { - DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l); - LST_Position(l, (LST_NODE*)pc); + DUL_PRESENTATIONCONTEXT* pc = reinterpret_cast<DUL_PRESENTATIONCONTEXT*>(LST_Head(l)); + LST_Position(l, reinterpret_cast<LST_NODE*>(pc)); while (pc) { - DicomTransferSyntax transferSyntax; if (pc->result == ASC_P_ACCEPTANCE) { + DicomTransferSyntax transferSyntax; + if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax)) { /*CLOG(TRACE, DICOM) << "C-GET SCP accepted: SOP class " << pc->abstractSyntax @@ -156,7 +157,7 @@ } } - pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l); + pc = reinterpret_cast<DUL_PRESENTATIONCONTEXT*>(LST_Next(l)); } }
--- a/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -85,12 +85,12 @@ } } - virtual unsigned int GetSubOperationCount() const + virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE { return instances_.size(); } - virtual Status DoNext() + virtual Status DoNext() ORTHANC_OVERRIDE { if (position_ >= instances_.size()) { @@ -169,12 +169,12 @@ } } - virtual unsigned int GetSubOperationCount() const + virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE { return countInstances_; } - virtual Status DoNext() + virtual Status DoNext() ORTHANC_OVERRIDE { if (position_ >= countInstances_) {
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -627,7 +627,7 @@ static void CreateDicomV1(ParsedDicomFile& dicom, - RestApiPostCall& call, + const RestApiPostCall& call, const Json::Value& request) { // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}'
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -72,7 +72,7 @@ void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, StoreStatus status, const std::string& instanceId) const { @@ -186,10 +186,10 @@ std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromBuffer(content)); toStore->SetOrigin(DicomInstanceOrigin::FromRest(call)); - std::string publicId; - try { + std::string publicId; + ServerContext::StoreResult result = context.Store(publicId, *toStore, StoreInstanceMode_Default); Json::Value info;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h Wed Nov 12 19:06:12 2025 +0100 @@ -103,7 +103,7 @@ // WARNING: "instanceId" can be different from // "instance.GetHasher().HashInstance()" if transcoding is enabled void AnswerStoredInstance(RestApiPostCall& call, - DicomInstanceToStore& instance, + const DicomInstanceToStore& instance, StoreStatus status, const std::string& instanceId) const;
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -494,7 +494,7 @@ std::string target; call.BodyToString(target); - SystemToolbox::WriteFile(dicom, target); + SystemToolbox::WriteFile(dicom, SystemToolbox::PathFromUtf8(target)); call.GetOutput().AnswerBuffer("{}", MimeType_Json); }
--- a/OrthancServer/Sources/OrthancWebDav.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancWebDav.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -597,7 +597,7 @@ virtual INode* CreateSubfolder(const std::string& path) = 0; public: - virtual ~InternalNode() + virtual ~InternalNode() ORTHANC_OVERRIDE { for (Children::iterator it = children_.begin(); it != children_.end(); ++it) { @@ -1221,10 +1221,10 @@ std::unique_ptr<DicomInstanceToStore> instance(DicomInstanceToStore::CreateFromBuffer(uncompressedFile)); instance->SetOrigin(DicomInstanceOrigin::FromWebDav()); - std::string publicId; - try { + std::string publicId; + context_.Store(publicId, *instance, StoreInstanceMode_Default); } catch (OrthancException& e) @@ -1256,7 +1256,7 @@ success = true; } } - catch (OrthancException& e) + catch (const OrthancException&) { } }
--- a/OrthancServer/Sources/OrthancWebDav.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/OrthancWebDav.h Wed Nov 12 19:06:12 2025 +0100 @@ -99,9 +99,9 @@ bool allowDicomDelete, bool allowUpload); - virtual ~OrthancWebDav() + virtual ~OrthancWebDav() ORTHANC_OVERRIDE { - Stop(); + OrthancWebDav::Stop(); } virtual bool IsExistingFolder(const UriComponents& path) ORTHANC_OVERRIDE;
--- a/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -272,7 +272,7 @@ currentPrivateCreator = ""; } - std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, currentPrivateCreator.c_str())); + std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, currentPrivateCreator)); cloned->copyFrom(*element); target->insert(cloned.release()); }
--- a/OrthancServer/Sources/ServerContext.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -1885,7 +1885,7 @@ return decoded.release(); } } - catch (OrthancException& e) + catch (const OrthancException&) { // ignore, we'll try other alternatives } } @@ -1898,7 +1898,7 @@ { decoded.reset(GetPlugins().Decode(buffer, size, frameIndex)); } - catch (OrthancException& e) + catch (const OrthancException&) { }
--- a/OrthancServer/Sources/ServerContext.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerContext.h Wed Nov 12 19:06:12 2025 +0100 @@ -319,7 +319,7 @@ bool readOnly, unsigned int maxConcurrentDcmtkTranscoder); - ~ServerContext(); + ~ServerContext() ORTHANC_OVERRIDE; void SetupJobsEngine(bool unitTesting, bool loadJobsFromDatabase);
--- a/OrthancServer/Sources/ServerIndex.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerIndex.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -236,7 +236,7 @@ { } - virtual ITransactionContext* Create() + virtual ITransactionContext* Create() ORTHANC_OVERRIDE { // There can be concurrent calls to this method, which is not an // issue because we simply create an object
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -191,13 +191,15 @@ boost::mutex availableInstancesMutex_; SharedMessageQueue instancesToPreload_; std::vector<boost::thread*> threads_; + bool loadersShouldStop_; public: ThreadedInstanceLoader(ServerContext& context, size_t threadCount, bool transcode, DicomTransferSyntax transferSyntax, unsigned int lossyQuality) : InstanceLoader(context, transcode, transferSyntax, lossyQuality), availableInstancesSemaphore_(0), - bufferedInstancesSemaphore_(3*threadCount) + bufferedInstancesSemaphore_(3*threadCount), + loadersShouldStop_(false) { for (size_t i = 0; i < threadCount; i++) { @@ -205,29 +207,45 @@ } } - virtual ~ThreadedInstanceLoader() + virtual ~ThreadedInstanceLoader() ORTHANC_OVERRIDE { - Clear(); + ThreadedInstanceLoader::Clear(); } virtual void Clear() ORTHANC_OVERRIDE { - for (size_t i = 0; i < threads_.size(); i++) - { - instancesToPreload_.Enqueue(NULL); - } - - for (size_t i = 0; i < threads_.size(); i++) + if (threads_.size() > 0) { - if (threads_[i]->joinable()) + LOG(INFO) << "Waiting for loader threads to complete"; + loadersShouldStop_ = true; // not need to protect this by a mutex. This is the only "writer" and all loaders are "readers" + + // unlock the loaders if they are waiting on this message queue (this happens when the job completes sucessfully) + for (size_t i = 0; i < threads_.size(); i++) { - threads_[i]->join(); + instancesToPreload_.Enqueue(NULL); + } + + // If the consumer stops e.g. because the HttpClient disconnected, we must make sure the loader threads are not blocked waiting for room in the bufferedInstances. + // If the loader threads have completed their jobs, this is harmless to release the bufferedInstances since they won't be used anymore. + for (size_t i = 0; i < threads_.size(); i++) + { + bufferedInstancesSemaphore_.Release(); } - delete threads_[i]; - } - threads_.clear(); - availableInstances_.clear(); + for (size_t i = 0; i < threads_.size(); i++) + { + if (threads_[i]->joinable()) + { + threads_[i]->join(); + } + delete threads_[i]; + } + + threads_.clear(); + availableInstances_.clear(); + + LOG(INFO) << "Waiting for loader threads to complete - done"; + } } static void PreloaderWorkerThread(ThreadedInstanceLoader* that) @@ -235,11 +253,14 @@ static uint16_t threadCounter = 0; Logging::SetCurrentThreadName(std::string("ARCH-LOAD-") + boost::lexical_cast<std::string>(threadCounter++)); + LOG(INFO) << "Loader thread has started"; + while (true) { std::unique_ptr<InstanceToPreload> instanceToPreload(dynamic_cast<InstanceToPreload*>(that->instancesToPreload_.Dequeue(0))); - if (instanceToPreload.get() == NULL) // that's the signal to exit the thread + if (instanceToPreload.get() == NULL || that->loadersShouldStop_) // that's the signal to exit the thread { + LOG(INFO) << "Loader thread has completed"; return; } @@ -269,6 +290,15 @@ } catch (OrthancException& e) { + LOG(ERROR) << "Failed to load instance " << instanceToPreload->GetId() << " error: " << e.GetDetails(); + boost::mutex::scoped_lock lock(that->availableInstancesMutex_); + // store a NULL result to notify that we could not read the instance + that->availableInstances_[instanceToPreload->GetId()] = boost::shared_ptr<std::string>(); + that->availableInstancesSemaphore_.Release(); + } + catch (...) + { + LOG(ERROR) << "Failed to load instance " << instanceToPreload->GetId() << " unknown error"; boost::mutex::scoped_lock lock(that->availableInstancesMutex_); // store a NULL result to notify that we could not read the instance that->availableInstances_[instanceToPreload->GetId()] = boost::shared_ptr<std::string>(); @@ -751,7 +781,6 @@ } void Apply(HierarchicalZipWriter& writer, - ServerContext& context, InstanceLoader& instanceLoader, DicomDirWriter* dicomDir, const std::string& dicomDirFolder, @@ -784,16 +813,11 @@ writer.OpenFile(filename_.c_str()); - std::unique_ptr<ParsedDicomFile> parsed; - writer.Write(content); if (dicomDir != NULL) { - if (parsed.get() == NULL) - { - parsed.reset(new ParsedDicomFile(content)); - } + std::unique_ptr<ParsedDicomFile> parsed(new ParsedDicomFile(content)); dicomDir->Add(dicomDirFolder, filename_, *parsed); } @@ -814,7 +838,6 @@ void ApplyInternal(HierarchicalZipWriter& writer, - ServerContext& context, InstanceLoader& instanceLoader, size_t index, DicomDirWriter* dicomDir, @@ -827,7 +850,7 @@ throw OrthancException(ErrorCode_ParameterOutOfRange); } - commands_[index]->Apply(writer, context, instanceLoader, dicomDir, dicomDirFolder, transcode, transferSyntax); + commands_[index]->Apply(writer, instanceLoader, dicomDir, dicomDirFolder, transcode, transferSyntax); } public: @@ -865,7 +888,6 @@ // "media" flavor (with DICOMDIR) void Apply(HierarchicalZipWriter& writer, - ServerContext& context, InstanceLoader& instanceLoader, size_t index, DicomDirWriter& dicomDir, @@ -873,18 +895,17 @@ bool transcode, DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, context, instanceLoader, index, &dicomDir, dicomDirFolder, transcode, transferSyntax); + ApplyInternal(writer, instanceLoader, index, &dicomDir, dicomDirFolder, transcode, transferSyntax); } // "archive" flavor (without DICOMDIR) void Apply(HierarchicalZipWriter& writer, - ServerContext& context, InstanceLoader& instanceLoader, size_t index, bool transcode, DicomTransferSyntax transferSyntax) const { - ApplyInternal(writer, context, instanceLoader, index, NULL, "", transcode, transferSyntax); + ApplyInternal(writer, instanceLoader, index, NULL, "", transcode, transferSyntax); } void AddOpenDirectory(const std::string& filename) @@ -1214,13 +1235,13 @@ if (isMedia_) { assert(dicomDir_.get() != NULL); - commands_.Apply(*zip_, context_, instanceLoader_, index, *dicomDir_, + commands_.Apply(*zip_, instanceLoader_, index, *dicomDir_, MEDIA_IMAGES_FOLDER, transcode, transferSyntax); } else { assert(dicomDir_.get() == NULL); - commands_.Apply(*zip_, context_, instanceLoader_, index, transcode, transferSyntax); + commands_.Apply(*zip_, instanceLoader_, index, transcode, transferSyntax); } } } @@ -1542,6 +1563,12 @@ synchronousTarget_.reset(); asynchronousTarget_.reset(); + + // clear the loader threads + if (instanceLoader_.get() != NULL) + { + instanceLoader_->Clear(); + } } }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h Wed Nov 12 19:06:12 2025 +0100 @@ -83,7 +83,7 @@ bool enableExtendedSopClass, ResourceType jobLevel); - virtual ~ArchiveJob(); + virtual ~ArchiveJob() ORTHANC_OVERRIDE; void AcquireSynchronousTarget(ZipWriter::IOutputStream* synchronousTarget);
--- a/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -46,7 +46,7 @@ const std::string& calledAet) { // this code is equivalent to OrthancStoreRequestHandler - ServerContext* context = reinterpret_cast<ServerContext*>(callbackContext); + DicomRetrieveScuBaseJob* job = reinterpret_cast<DicomRetrieveScuBaseJob*>(callbackContext); std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dataset)); @@ -56,7 +56,14 @@ (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str())); std::string id; - ServerContext::StoreResult result = context->Store(id, *toStore, StoreInstanceMode_Default); + ServerContext::StoreResult result = job->GetContext().Store(id, *toStore, StoreInstanceMode_Default); + + // keep track of the received instances in the job + if (result.GetStatus() == StoreStatus_Success || result.GetStatus() == StoreStatus_AlreadyStored) + { + job->AddReceivedInstance(id); + } + return result.GetCStoreStatusCode(); } @@ -68,11 +75,12 @@ if (connection_.get() == NULL) { std::set<std::string> sopClassesToPropose; - std::set<std::string> acceptedSopClasses; std::list<DicomTransferSyntax> proposedTransferSyntaxes; if (sopClassesFromResourcesToRetrieve_.size() > 0) { + std::set<std::string> acceptedSopClasses; + context_.GetAcceptedSopClasses(acceptedSopClasses, 0); // keep the sop classes from the resources to retrieve only if they are accepted by Orthanc @@ -99,7 +107,7 @@ } connection_->SetProgressListener(this); - connection_->Get(findAnswer, InstanceReceivedHandler, &context_); + connection_->Get(findAnswer, InstanceReceivedHandler, this); } void DicomGetScuJob::AddFindAnswer(const DicomMap& answer)
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -47,7 +47,9 @@ } connection_->SetProgressListener(this); - connection_->Move(targetAet_, findAnswer); + + // we use a unique message ID in order to know to which Move request a stored instance relates to. + connection_->Move(targetAet_, findAnswer, GetMessageId(GetParameters().GetLocalApplicationEntityTitle())); }
--- a/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -29,6 +29,7 @@ #include <dcmtk/dcmnet/dimse.h> #include <algorithm> #include "../../../OrthancFramework/Sources/Logging.h" +#include <boost/thread/mutex.hpp> static const char* const LOCAL_AET = "LocalAet"; static const char* const QUERY = "Query"; @@ -36,8 +37,106 @@ static const char* const REMOTE = "Remote"; static const char* const TIMEOUT = "Timeout"; +static std::map<std::string, Orthanc::SetOfCommandsJob::ICommand*> messagesRegistry; +static uint16_t messageRegistryCurrentId = 1000; +static boost::mutex messageRegistryMutex; + namespace Orthanc { + std::string GetKey(const std::string& aet, uint16_t messageId) + { + return aet + "-" + boost::lexical_cast<std::string>(messageId); + } + + uint16_t DicomRetrieveScuBaseJob::GetMessageId(const std::string& localAet) + { + assert(currentCommand_ != NULL); + boost::mutex::scoped_lock lock(messageRegistryMutex); + + // Each resource retrieval (command) has its own messageId. + // We start at 1000 to clearly differentiate them from other messages. We can actually use ANY value between 0 & 65535. + messageRegistryCurrentId = std::max(1000, (messageRegistryCurrentId + 1) % 0xFFFF); + messagesRegistry[GetKey(localAet, messageRegistryCurrentId)] = currentCommand_; + + return messageRegistryCurrentId; + } + + void DicomRetrieveScuBaseJob::AddReceivedInstanceFromCStore(uint16_t originatorMessageId, + const std::string& originatorAet, + const std::string& instanceId) + { + boost::mutex::scoped_lock lock(messageRegistryMutex); + + std::string key = GetKey(originatorAet, originatorMessageId); + + if (messagesRegistry.find(key) != messagesRegistry.end()) + { + dynamic_cast<DicomRetrieveScuBaseJob::Command*>(messagesRegistry[key])->AddReceivedInstance(instanceId); + } + } + + DicomRetrieveScuBaseJob::Command::~Command() + { + // remove the command from the messageRegistry + boost::mutex::scoped_lock lock(messageRegistryMutex); + + for (std::map<std::string, Orthanc::SetOfCommandsJob::ICommand*>::const_iterator it = messagesRegistry.begin(); + it != messagesRegistry.end(); ++it) + { + if (it->second == this) + { + messagesRegistry.erase(it->first); + return; + } + } + } + + bool DicomRetrieveScuBaseJob::Command::Execute(const std::string &jobId) + { + try + { + that_.currentCommand_ = this; + that_.Retrieve(*findAnswer_); // here + } + catch (OrthancException& e) + { + dimseErrorStatus_ = e.GetDimseErrorStatus(); + throw e; + } + return true; + } + + void DicomRetrieveScuBaseJob::Command::Serialize(Json::Value &target) const + { + findAnswer_->Serialize(target["Query"]); + target["DimseErrorStatus"] = dimseErrorStatus_; + SerializationToolbox::WriteListOfStrings(target, receivedInstancesIds_, "ReceivedInstancesIds"); + } + + SetOfCommandsJob::ICommand* DicomRetrieveScuBaseJob::Unserializer::Unserialize(const Json::Value &source) const + { + DicomMap findAnswer; + + if (!source.isMember("Query")) // old format before 1.12.10, just in case we need to read old jobs after an upgrade + { + findAnswer.Unserialize(source); + return new DicomRetrieveScuBaseJob::Command(that_, findAnswer); + } + + findAnswer.Unserialize(source["Query"]); + + std::unique_ptr<DicomRetrieveScuBaseJob::Command> command(new DicomRetrieveScuBaseJob::Command(that_, findAnswer)); + command->SetDimseErrorStatus(static_cast<uint16_t>(source["DimseErrorStatus"].asUInt())); + + std::list<std::string> receivedInstancesIds; + SerializationToolbox::ReadListOfStrings(receivedInstancesIds, source, "ReceivedInstancesIds"); + for (std::list<std::string>::const_iterator it = receivedInstancesIds.begin(); it != receivedInstancesIds.end(); ++it) + { + command->AddReceivedInstance(*it); + } + + return command.release(); + } static void AddToQuery(DicomFindAnswers& query, const DicomMap& item) @@ -152,6 +251,20 @@ value[QUERY] = Json::objectValue; query_.ToJson(value[QUERY], queryFormat_); + + value["Details"] = Json::arrayValue; + + for (size_t i = 0; i < GetCommandsCount(); ++i) + { + const DicomRetrieveScuBaseJob::Command& command = dynamic_cast<const DicomRetrieveScuBaseJob::Command&>(GetCommand(i)); + + Json::Value v; + v["DimseErrorStatus"] = command.GetDimseErrorStatus(); + query_.ToJson(v["Query"], i, DicomToJsonFormat_Short); + SerializationToolbox::WriteListOfStrings(v, command.GetReceivedInstancesIds(), "ReceivedInstancesIds"); + + value["Details"].append(v); + } } @@ -162,7 +275,8 @@ nbRemainingSubOperations_(0), nbCompletedSubOperations_(0), nbFailedSubOperations_(0), - nbWarningSubOperations_(0) + nbWarningSubOperations_(0), + currentCommand_(NULL) { } @@ -172,12 +286,13 @@ SetOfCommandsJob(new Unserializer(*this), serialized), context_(context), parameters_(DicomAssociationParameters::UnserializeJob(serialized)), - query_(true), + query_(false /* this is not for worklists */), queryFormat_(DicomToJsonFormat_Short), nbRemainingSubOperations_(0), nbCompletedSubOperations_(0), nbFailedSubOperations_(0), - nbWarningSubOperations_(0) + nbWarningSubOperations_(0), + currentCommand_(NULL) { if (serialized.isMember(QUERY)) { @@ -245,4 +360,12 @@ return float(nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_) / float(totalOperations); } + + void DicomRetrieveScuBaseJob::AddReceivedInstance(const std::string& instanceId) + { + if (currentCommand_ != NULL) + { + currentCommand_->AddReceivedInstance(instanceId); + } + } }
--- a/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h Wed Nov 12 19:06:12 2025 +0100 @@ -36,29 +36,50 @@ class DicomRetrieveScuBaseJob : public SetOfCommandsJob, public DicomControlUserConnection::IProgressListener { protected: + uint16_t GetMessageId(const std::string& localAet); + class Command : public SetOfCommandsJob::ICommand { private: DicomRetrieveScuBaseJob &that_; std::unique_ptr<DicomMap> findAnswer_; + uint16_t dimseErrorStatus_; + std::list<std::string> receivedInstancesIds_; + public: Command(DicomRetrieveScuBaseJob &that, const DicomMap &findAnswer) : that_(that), - findAnswer_(findAnswer.Clone()) + findAnswer_(findAnswer.Clone()), + dimseErrorStatus_(0) { } - virtual bool Execute(const std::string &jobId) ORTHANC_OVERRIDE + virtual ~Command() ORTHANC_OVERRIDE; + + virtual bool Execute(const std::string &jobId) ORTHANC_OVERRIDE; + + virtual void Serialize(Json::Value &target) const ORTHANC_OVERRIDE; + + uint16_t GetDimseErrorStatus() const { - that_.Retrieve(*findAnswer_); - return true; + return dimseErrorStatus_; } - virtual void Serialize(Json::Value &target) const ORTHANC_OVERRIDE + void SetDimseErrorStatus(uint16_t dimseErrorStatus) + { + dimseErrorStatus_ = dimseErrorStatus; + } + + void AddReceivedInstance(const std::string& instanceId) { - findAnswer_->Serialize(target); + receivedInstancesIds_.push_back(instanceId); + } + + const std::list<std::string>& GetReceivedInstancesIds() const + { + return receivedInstancesIds_; } }; @@ -73,12 +94,7 @@ { } - virtual ICommand *Unserialize(const Json::Value &source) const ORTHANC_OVERRIDE - { - DicomMap findAnswer; - findAnswer.Unserialize(source); - return new Command(that_, findAnswer); - } + virtual ICommand *Unserialize(const Json::Value &source) const ORTHANC_OVERRIDE; }; ServerContext &context_; @@ -94,6 +110,8 @@ uint16_t nbFailedSubOperations_; uint16_t nbWarningSubOperations_; + Command* currentCommand_; + virtual void Retrieve(const DicomMap &findAnswer) = 0; explicit DicomRetrieveScuBaseJob(ServerContext &context); @@ -145,5 +163,15 @@ return true; } + ServerContext& GetContext() + { + return context_; + } + + void AddReceivedInstance(const std::string& instanceId); + + static void AddReceivedInstanceFromCStore(uint16_t originatorMessageId, + const std::string& originatorAet, + const std::string& instanceId); }; }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -185,7 +185,7 @@ for (std::set<std::string>::const_iterator it = instancesToReconstruct_.begin(); it != instancesToReconstruct_.end(); ++it) { ServerContext::DicomCacheLocker locker(GetContext(), *it); - ParsedDicomFile& modifiedDicom = locker.GetDicom(); + const ParsedDicomFile& modifiedDicom = locker.GetDicom(); GetContext().GetIndex().ReconstructInstance(modifiedDicom, false, ResourceType_Instance /* dummy */); } @@ -219,7 +219,7 @@ try { ServerContext::DicomCacheLocker locker(GetContext(), instance); - ParsedDicomFile& original = locker.GetDicom(); + const ParsedDicomFile& original = locker.GetDicom(); originalHasher.reset(new DicomInstanceHasher(original.GetHasher())); modified.reset(original.Clone(true)); @@ -719,7 +719,7 @@ } // and we must make sure that we overwite them with the modified resources - if (IsKeepSource() && !GetContext().IsOverwriteInstances()) + if (!GetContext().IsOverwriteInstances()) { throw OrthancException(ErrorCode_BadRequest, "When keeping StudyInstanceUID, SeriesInstanceUID and SOPInstanceUID tag, you must have the 'OverwriteInstances' Orthanc configuration set to true in order to replace the modified resources");
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -375,6 +375,8 @@ } else { + SetOfCommandsJob::Reserve(size); + sopClassUids_.reserve(size); sopInstanceUids_.reserve(size); }
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h Wed Nov 12 19:06:12 2025 +0100 @@ -78,7 +78,7 @@ StorageCommitmentScpJob(ServerContext& context, const Json::Value& serialized); - void Reserve(size_t size); + virtual void Reserve(size_t size) ORTHANC_OVERRIDE; void AddInstance(const std::string& sopClassUid, const std::string& sopInstanceUid);
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h Wed Nov 12 19:06:12 2025 +0100 @@ -84,7 +84,7 @@ bool hasTrailingStep, bool defaultKeepSource); - virtual ~ThreadedSetOfInstancesJob(); + virtual ~ThreadedSetOfInstancesJob() ORTHANC_OVERRIDE; protected: virtual bool HandleInstance(const std::string& instance) = 0;
--- a/OrthancServer/Sources/SimpleInstanceOrdering.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/SimpleInstanceOrdering.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -96,7 +96,6 @@ for (size_t i = 0; i < response.GetSize(); ++i) { const FindResponse::Resource& resource = response.GetResourceByIndex(i); - std::string instanceId = resource.GetIdentifier(); std::string strIndexInSeries; uint32_t indexInSeries = 0; @@ -110,6 +109,8 @@ if (resource.LookupAttachment(fileInfo, revisionNotUsed, FileContentType_Dicom)) { + std::string instanceId = resource.GetIdentifier(); + allIndexInSeries.insert(indexInSeries); instances_.push_back(new SimpleInstanceOrdering::Instance(instanceId, indexInSeries, fileInfo)); }
--- a/OrthancServer/Sources/SliceOrdering.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/SliceOrdering.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -192,12 +192,12 @@ } hasNormal_ = ComputeNormal(normal_, instance); - - std::string s; hasIndexInSeries_ = false; try { + std::string s; + if (index.LookupMetadata(s, instanceId, ResourceType_Instance, MetadataType_Instance_IndexInSeries)) { indexInSeries_ = boost::lexical_cast<size_t>(Toolbox::StripSpaces(s));
--- a/OrthancServer/Sources/main.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/Sources/main.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -47,6 +47,7 @@ #include "ServerContext.h" #include "ServerEnumerations.h" #include "ServerJobs/StorageCommitmentScpJob.h" +#include "ServerJobs/DicomRetrieveScuBaseJob.h" #include "ServerToolbox.h" #include "StorageCommitmentReports.h" @@ -86,7 +87,9 @@ virtual uint16_t Handle(DcmDataset& dicom, const std::string& remoteIp, const std::string& remoteAet, - const std::string& calledAet) ORTHANC_OVERRIDE + const std::string& calledAet, + uint16_t originatorMessageId, + const std::string& originatorAet) ORTHANC_OVERRIDE { std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dicom)); @@ -97,6 +100,13 @@ std::string id; ServerContext::StoreResult result = context_.Store(id, *toStore, StoreInstanceMode_Default); + + if (result.GetStatus() == StoreStatus_Success || result.GetStatus() == StoreStatus_AlreadyStored) + { + // In case this C-Store was triggered from a C-Move job, keep track of the instances that have been received + DicomRetrieveScuBaseJob::AddReceivedInstanceFromCStore(originatorMessageId, originatorAet, id); + } + return result.GetCStoreStatusCode(); } @@ -2042,7 +2052,7 @@ } else { - SystemToolbox::WriteFile(configurationSample, target); + SystemToolbox::WriteFile(configurationSample, SystemToolbox::PathFromUtf8(target)); } return 0; } @@ -2093,7 +2103,7 @@ } else { - SystemToolbox::WriteFile(s, target); + SystemToolbox::WriteFile(s, SystemToolbox::PathFromUtf8(target)); } return 0; } @@ -2127,7 +2137,7 @@ } else { - SystemToolbox::WriteFile(cheatsheet, target); + SystemToolbox::WriteFile(cheatsheet, SystemToolbox::PathFromUtf8(target)); } return 0; }
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Tue Oct 28 10:29:11 2025 +0100 +++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp Wed Nov 12 19:06:12 2025 +0100 @@ -1274,10 +1274,12 @@ TEST_F(OrthancJobsSerialization, DicomMoveScuJob) { Json::Value command = Json::objectValue; - command["0008,0005"]["Type"] = "String"; - command["0008,0005"]["Content"] = "ISO_IR 100"; - command["0010,0020"]["Type"] = "String"; - command["0010,0020"]["Content"] = "1234"; + command["Query"]["0008,0005"]["Type"] = "String"; + command["Query"]["0008,0005"]["Content"] = "ISO_IR 100"; + command["Query"]["0010,0020"]["Type"] = "String"; + command["Query"]["0010,0020"]["Content"] = "1234"; + command["DimseErrorStatus"] = 0; + command["ReceivedInstancesIds"] = Json::arrayValue; Json::Value query = Json::objectValue; query["0010,0020"] = "456";
--- a/TODO Tue Oct 28 10:29:11 2025 +0100 +++ b/TODO Wed Nov 12 19:06:12 2025 +0100 @@ -64,7 +64,8 @@ https://discourse.orthanc-server.org/t/instances-id-content-api-results-are-different-in-docker-swarm-replicas-of-orthanc/4582 * 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. + 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": { @@ -72,7 +73,9 @@ "Hashing": "sha1"} } * Reuse the ThreadedInstanceLoader from the ArchiveJob in C-Store, peer/store ... to optimize loading time when working with an object storage. - +* Implement a SharedMessageQueue with a max size -> that would greatly simplify the flow control in the + ThreadedInstanceLoader (also used in DicomWeb). + ============================ Documentation (Orthanc Book) @@ -185,6 +188,14 @@ DICOM ===== +---------- +Short-term +---------- + +* Investigate stucked C-MOVE that seems to block the job engine: + https://discourse.orthanc-server.org/t/frozen-jobs-and-rest-api-not-responding-after-stuck-c-move/6280/11 + + -------- Mid-term -------- @@ -206,11 +217,20 @@ * Log outgoing C-Find queries * Support other Transfer Syntaxes in the Worklist plugin: https://discourse.orthanc-server.org/t/could-you-please-create-an-option-to-set-the-transfer-syntax-in-the-worklist-plugin-currently-little-endian-explicit-is-fixed/4871 +* For C-Find (including C-Find for worklists) Orthanc always uses the "DefaultEncoding" in the response even if it needs to + return e.g. an Utf-8 PatientName that can not be represented in the the "DefaultEncoding" e.g. "Latin1". + According to the standard (PS 3.4 C.4.1.1.3.2): + Conditionally, the Attribute Specific Character Set (0008,0005). This Attribute shall be included if expanded or replacement + character sets may be used in any of the Attributes in the Response Identifier. It shall not be included otherwise. + The C-FIND SCP is not required to return responses in the Specific Character Set requested by the SCU if that character set + is not supported by the SCP. The SCP may return responses with a different Specific Character Set. + Therefore, Orthanc should: + - Preferrably use the same SpecificCharacterSet as the one used in the C-Find request. + - If Orthanc is unable to represent a string with this encoding, it shall switch to UTF-8 in the response * Deidentify Trace level logs: https://discourse.orthanc-server.org/t/dicom-log-deidentification-at-trace-log-level/5563 We should implement something like GetDeidentifiedContent(const DcmDataSet& dataset) that would reproduce DcmDataSet::print but hide individual elements that contain PHI. - --------- Long-term --------- @@ -247,7 +267,7 @@ * (3) Check out rapidjson: https://github.com/miloyip/nativejson-benchmark * Long-shot & not sure it is even feasible at all: try to reduce memory usage by implementing streaming - when receiving DICOM instances from the Rest API or from DICOM and store files directly to disk as they + when receiving DICOM instances from the REST API or from DICOM and store files directly to disk as they are received. Note that this would likely require rewriting large parts of Orthanc. Note sure that would be compatible with Transcoding. Use case: receiving 10 1GB instances in parallel can consume up to 20 GB
