# HG changeset patch # User Alain Mazy # Date 1639472776 -3600 # Node ID 465bf098554bf116e6c8cf78467515bdc18da234 # Parent 62f2731094ae981cdc2657526c3ccd50ccecb574 new callback: orthanc.RegisterReceivedInstanceCallback() diff -r 62f2731094ae -r 465bf098554b CMakeLists.txt --- a/CMakeLists.txt Fri Dec 10 11:08:51 2021 +0100 +++ b/CMakeLists.txt Tue Dec 14 10:06:16 2021 +0100 @@ -177,6 +177,7 @@ Sources/PythonModule.cpp Sources/PythonObject.cpp Sources/PythonString.cpp + Sources/ReceivedInstanceCallback.cpp Sources/RestCallbacks.cpp Sources/StorageArea.cpp diff -r 62f2731094ae -r 465bf098554b CodeAnalysis/Dockerfile --- a/CodeAnalysis/Dockerfile Fri Dec 10 11:08:51 2021 +0100 +++ b/CodeAnalysis/Dockerfile Tue Dec 14 10:06:16 2021 +0100 @@ -7,7 +7,8 @@ RUN apt-get --assume-yes install python-pip RUN apt-get --assume-yes install clang-4.0 -RUN pip install pystache +# force pystache 0.5.0 (last version supported for python 2.7) +RUN pip install pystache==0.5.0 RUN mkdir /source RUN mkdir /target diff -r 62f2731094ae -r 465bf098554b NEWS --- a/NEWS Fri Dec 10 11:08:51 2021 +0100 +++ b/NEWS Tue Dec 14 10:06:16 2021 +0100 @@ -5,6 +5,7 @@ * New functions from the SDK wrapped in Python: - orthanc.RegisterIncomingCStoreInstanceFilter() + - orthanc.RegisterReceivedInstanceCallback() Version 3.4 (2021-08-31) ======================== diff -r 62f2731094ae -r 465bf098554b Resources/Orthanc/Sdk-1.9.8/orthanc/OrthancCPlugin.h --- a/Resources/Orthanc/Sdk-1.9.8/orthanc/OrthancCPlugin.h Fri Dec 10 11:08:51 2021 +0100 +++ b/Resources/Orthanc/Sdk-1.9.8/orthanc/OrthancCPlugin.h Tue Dec 14 10:06:16 2021 +0100 @@ -84,6 +84,7 @@ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2021 Osimis S.A., Belgium + * Copyright (C) 2021-2021 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 @@ -462,6 +463,7 @@ _OrthancPluginService_RegisterTranscoderCallback = 1015, /* New in Orthanc 1.7.0 */ _OrthancPluginService_RegisterStorageArea2 = 1016, /* New in Orthanc 1.9.0 */ _OrthancPluginService_RegisterIncomingCStoreInstanceFilter = 1017, /* New in Orthanc 1.9.8 */ + _OrthancPluginService_RegisterReceivedInstanceCallback = 1018, /* New in Orthanc 1.9.8 */ /* Sending answers to REST calls */ _OrthancPluginService_AnswerBuffer = 2000, @@ -1000,7 +1002,19 @@ is already in use */ } OrthancPluginStorageCommitmentFailureReason; - + + /** + * The return value of ReceivedInstanceCallback + **/ + typedef enum + { + OrthancPluginReceivedInstanceCallbackResult_KeepAsIs = 1, /*!< Keep the instance as is */ + OrthancPluginReceivedInstanceCallbackResult_Modified = 2, /*!< Modified the instance */ + OrthancPluginReceivedInstanceCallbackResult_Discard = 3, /*!< Tell Orthanc to discard the instance */ + + _OrthancPluginReceivedInstanceCallbackResult_INTERNAL = 0x7fffffff + } OrthancPluginReceivedInstanceCallbackResult; + /** * @brief A 32-bit memory buffer allocated by the core system of Orthanc. @@ -7822,6 +7836,73 @@ } /** + * @brief Callback to possibly modify a DICOM instance received + * by Orthanc through any source (C-Store or Rest API) + * + * Signature of a callback function that is triggered whenever + * Orthanc receives a new DICOM instance (through DICOM protocol or + * Rest API), and that answers a possibly modified version of the + * DICOM that should be stored in Orthanc. + * + * This callback is called immediately after receiption: before + * transcoding and before filtering (FilterIncomingInstance). + * + * @param receivedDicomBuffer A buffer containing the received DICOM (input). + * @param receivedDicomBufferSize The size of the received DICOM (input) + * @param modifiedDicomBuffer A buffer containing the modified DICOM (output). + * This buffer will be freed by the Orthanc Core and must have + * been allocated by malloc in your plugin or by Orthanc core through + * a plugin method. + * @param modifiedDicomBufferSize The size of the modified DICOM (output) + * @return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs to accept the instance as is + * OrthancPluginReceivedInstanceCallbackResult_Modified to store the modified DICOM + * OrthancPluginReceivedInstanceCallbackResult_Discard to tell Orthanc to discard the instance + * @ingroup Callback + **/ + typedef OrthancPluginReceivedInstanceCallbackResult (*OrthancPluginReceivedInstanceCallback) ( + const void* receivedDicomBuffer, + uint64_t receivedDicomBufferSize, + void** modifiedDicomBuffer, + uint64_t* modifiedDicomBufferSize + ); + + + typedef struct + { + OrthancPluginReceivedInstanceCallback callback; + } _OrthancPluginReceivedInstanceCallback; + + /** + * @brief Register a callback to possibly modify a DICOM instance received + * by Orthanc through any source (C-Store or Rest API) + * + * + * @warning Your callback function will be called synchronously with + * the core of Orthanc. This implies that deadlocks might emerge if + * you call other core primitives of Orthanc in your callback (such + * deadlocks are particular visible in the presence of other plugins + * or Lua scripts). It is thus strongly advised to avoid any call to + * the REST API of Orthanc in the callback. If you have to call + * other primitives of Orthanc, you should make these calls in a + * separate thread, passing the pending events to be processed + * through a message queue. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param callback The callback. + * @return 0 if success, other value if error. + * @ingroup Callbacks + **/ + ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterReceivedInstanceCallback( + OrthancPluginContext* context, + OrthancPluginReceivedInstanceCallback callback) + { + _OrthancPluginReceivedInstanceCallback params; + params.callback = callback; + + return context->InvokeService(context, _OrthancPluginService_RegisterReceivedInstanceCallback, ¶ms); + } + + /** * @brief Get the transfer syntax of a DICOM file. * * This function returns a pointer to a newly created string that diff -r 62f2731094ae -r 465bf098554b Sources/Autogenerated/sdk.cpp --- a/Sources/Autogenerated/sdk.cpp Fri Dec 10 11:08:51 2021 +0100 +++ b/Sources/Autogenerated/sdk.cpp Tue Dec 14 10:06:16 2021 +0100 @@ -41,6 +41,7 @@ #include "./sdk_OrthancPluginDicomToJsonFormat.impl.h" #include "./sdk_OrthancPluginMetricsType.impl.h" #include "./sdk_OrthancPluginValueRepresentation.impl.h" +#include "./sdk_OrthancPluginReceivedInstanceCallbackResult.impl.h" #include "./sdk_OrthancPluginImageFormat.impl.h" #include "./sdk_OrthancPluginChangeType.impl.h" @@ -80,6 +81,7 @@ RegisterOrthancPluginDicomToJsonFormatEnumeration(module); RegisterOrthancPluginMetricsTypeEnumeration(module); RegisterOrthancPluginValueRepresentationEnumeration(module); + RegisterOrthancPluginReceivedInstanceCallbackResultEnumeration(module); RegisterOrthancPluginImageFormatEnumeration(module); RegisterOrthancPluginChangeTypeEnumeration(module); diff -r 62f2731094ae -r 465bf098554b Sources/Autogenerated/sdk_OrthancPluginReceivedInstanceCallbackResult.impl.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/Autogenerated/sdk_OrthancPluginReceivedInstanceCallbackResult.impl.h Tue Dec 14 10:06:16 2021 +0100 @@ -0,0 +1,77 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2020-2021 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +typedef struct +{ + PyObject_HEAD +} sdk_OrthancPluginReceivedInstanceCallbackResult_Object; + + +/** + * Static global structure => the fields that are beyond the last + * initialized field are set to zero. + * https://stackoverflow.com/a/11152199/881731 + **/ +static PyTypeObject sdk_OrthancPluginReceivedInstanceCallbackResult_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "orthanc.ReceivedInstanceCallbackResult", /* tp_name */ + sizeof(sdk_OrthancPluginReceivedInstanceCallbackResult_Object), /* tp_basicsize */ +}; + + +void RegisterOrthancPluginReceivedInstanceCallbackResultEnumeration(PyObject* module) +{ + sdk_OrthancPluginReceivedInstanceCallbackResult_Type.tp_new = PyType_GenericNew; + sdk_OrthancPluginReceivedInstanceCallbackResult_Type.tp_flags = Py_TPFLAGS_DEFAULT; + sdk_OrthancPluginReceivedInstanceCallbackResult_Type.tp_doc = "Generated from C enumeration OrthancPluginOrthancPluginReceivedInstanceCallbackResult"; + + sdk_OrthancPluginReceivedInstanceCallbackResult_Type.tp_dict = PyDict_New(); + + if (PyType_Ready(&sdk_OrthancPluginReceivedInstanceCallbackResult_Type) < 0) + { + OrthancPlugins::LogError("Cannot register Python enumeration: OrthancPluginReceivedInstanceCallbackResult"); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } + + /** + * Declare constants here (static members = class attributes) + * https://stackoverflow.com/a/8017906/881731 + * + * "Static and class methods can be defined in tp_methods by adding + * METH_STATIC or METH_CLASS to the ml_flags field of the + * PyMethodDef structure. This is equivalent to @staticmethod and + * @classmethod decorators." + * + * "Class attributes can be added by setting the tp_dict to a + * dictionary with these attributes before calling PyType_Ready() + * (in your module initialization function)." + **/ + + PyDict_SetItemString(sdk_OrthancPluginReceivedInstanceCallbackResult_Type.tp_dict, "KEEP_AS_IS", PyLong_FromLong(1)); + PyDict_SetItemString(sdk_OrthancPluginReceivedInstanceCallbackResult_Type.tp_dict, "MODIFIED", PyLong_FromLong(2)); + PyDict_SetItemString(sdk_OrthancPluginReceivedInstanceCallbackResult_Type.tp_dict, "DISCARD", PyLong_FromLong(3)); + + Py_INCREF(&sdk_OrthancPluginReceivedInstanceCallbackResult_Type); + if (PyModule_AddObject(module, "ReceivedInstanceCallbackResult", (PyObject *)&sdk_OrthancPluginReceivedInstanceCallbackResult_Type) < 0) + { + OrthancPlugins::LogError("Cannot register Python enumeration: OrthancPluginReceivedInstanceCallbackResult"); + Py_DECREF(&sdk_OrthancPluginReceivedInstanceCallbackResult_Type); + ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError); + } +} diff -r 62f2731094ae -r 465bf098554b Sources/Plugin.cpp --- a/Sources/Plugin.cpp Fri Dec 10 11:08:51 2021 +0100 +++ b/Sources/Plugin.cpp Tue Dec 14 10:06:16 2021 +0100 @@ -29,6 +29,7 @@ #include "OnChangeCallback.h" #include "OnStoredInstanceCallback.h" #include "IncomingInstanceFilter.h" +#include "ReceivedInstanceCallback.h" #include "StorageArea.h" #include "RestCallbacks.h" @@ -383,6 +384,11 @@ functions.push_back(f); } + { + PyMethodDef f = { "RegisterReceivedInstanceCallback", RegisterReceivedInstanceCallback, METH_VARARGS, "" }; + functions.push_back(f); + } + /** * Append all the global functions that were automatically generated **/ diff -r 62f2731094ae -r 465bf098554b Sources/ReceivedInstanceCallback.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/ReceivedInstanceCallback.cpp Tue Dec 14 10:06:16 2021 +0100 @@ -0,0 +1,140 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2020-2021 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#include "ReceivedInstanceCallback.h" + +#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h" +#include "ICallbackRegistration.h" +#include "PythonString.h" + +#include // For std::unique_ptr<> + +static PyObject* receivedInstanceCallback_ = NULL; + + +static OrthancPluginReceivedInstanceCallbackResult ReceivedInstanceCallback( + const void* receivedDicomBuffer, + uint64_t receivedDicomBufferSize, + void** modifiedDicomBuffer, + uint64_t* modifiedDicomBufferSize) +{ + try + { + PythonLock lock; + + PythonObject args(lock, PyTuple_New(1)); + + PyTuple_SetItem(args.GetPyObject(), 0, PyBytes_FromStringAndSize(reinterpret_cast(receivedDicomBuffer), receivedDicomBufferSize)); + + PythonObject result(lock, PyObject_CallObject(receivedInstanceCallback_, args.GetPyObject())); + + std::string traceback; + if (lock.HasErrorOccurred(traceback)) + { + OrthancPlugins::LogError("Error in the Python received instance callback, traceback:\n" + traceback); + return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs; + } + else if (!PyTuple_Check(result.GetPyObject()) || PyTuple_Size(result.GetPyObject()) != 2) + { + OrthancPlugins::LogError("The Python received instance callback has not returned a tuple as expected"); + return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs; + } + else + { + PyObject* returnCode = PyTuple_GET_ITEM(result.GetPyObject(), 0); + PyObject* modifiedDicom = PyTuple_GET_ITEM(result.GetPyObject(), 1); + + if (!PyLong_Check(returnCode)) + { + OrthancPlugins::LogError("The Python received instance callback has not returned an int as the first element of the return tuple"); + return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs; + } + + OrthancPluginReceivedInstanceCallbackResult resultCode = static_cast(PyLong_AsLong(returnCode)); + + if (resultCode == OrthancPluginReceivedInstanceCallbackResult_KeepAsIs || + resultCode == OrthancPluginReceivedInstanceCallbackResult_Discard) + { + return resultCode; + } + + char* pythonBuffer = NULL; + Py_ssize_t pythonSize = 0; + if (PyBytes_AsStringAndSize(modifiedDicom, &pythonBuffer, &pythonSize) == 1) + { + OrthancPlugins::LogError("Cannot access the byte buffer returned by the Python received instance callback"); + return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs; + } + else + { + if (pythonSize == 0) + { + *modifiedDicomBuffer = NULL; + *modifiedDicomBufferSize = 0; + return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs; + } + else + { + *modifiedDicomBuffer = malloc(pythonSize); + *modifiedDicomBufferSize = pythonSize; + if (*modifiedDicomBuffer == NULL) + { + OrthancPlugins::LogError("Cannot allocate memory in the Python received instance callback"); + return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs; + } + + memcpy(*modifiedDicomBuffer, pythonBuffer, pythonSize); + return OrthancPluginReceivedInstanceCallbackResult_Modified; + } + } + } + } + catch (OrthancPlugins::PluginException& e) + { + OrthancPlugins::LogError("Error in the Python received instance callback: " + std::string(e.What(OrthancPlugins::GetGlobalContext()))); + } + + return OrthancPluginReceivedInstanceCallbackResult_KeepAsIs; +} + + +PyObject* RegisterReceivedInstanceCallback(PyObject* module, PyObject* args) +{ + // The GIL is locked at this point (no need to create "PythonLock") + + class Registration : public ICallbackRegistration + { + public: + virtual void Register() ORTHANC_OVERRIDE + { + OrthancPluginRegisterReceivedInstanceCallback( + OrthancPlugins::GetGlobalContext(), ReceivedInstanceCallback); + } + }; + + Registration registration; + return ICallbackRegistration::Apply( + registration, args, receivedInstanceCallback_, "Python received instance callback"); +} + + +void FinalizeReceivedInstanceCallback() +{ + ICallbackRegistration::Unregister(receivedInstanceCallback_); +} diff -r 62f2731094ae -r 465bf098554b Sources/ReceivedInstanceCallback.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Sources/ReceivedInstanceCallback.h Tue Dec 14 10:06:16 2021 +0100 @@ -0,0 +1,26 @@ +/** + * Python plugin for Orthanc + * Copyright (C) 2020-2021 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero 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 + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "PythonHeaderWrapper.h" + +void FinalizeReceivedInstanceCallback(); + +PyObject* RegisterReceivedInstanceCallback(PyObject* module, PyObject* args);