changeset 99:465bf098554b

new callback: orthanc.RegisterReceivedInstanceCallback()
author Alain Mazy <am@osimis.io>
date Tue, 14 Dec 2021 10:06:16 +0100
parents 62f2731094ae
children e2b2e1d4e1bb
files CMakeLists.txt CodeAnalysis/Dockerfile NEWS Resources/Orthanc/Sdk-1.9.8/orthanc/OrthancCPlugin.h Sources/Autogenerated/sdk.cpp Sources/Autogenerated/sdk_OrthancPluginReceivedInstanceCallbackResult.impl.h Sources/Plugin.cpp Sources/ReceivedInstanceCallback.cpp Sources/ReceivedInstanceCallback.h
diffstat 9 files changed, 337 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- 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
 
--- 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
--- 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)
 ========================
--- 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, &params);
+  }
+
+  /**
    * @brief Get the transfer syntax of a DICOM file.
    *
    * This function returns a pointer to a newly created string that
--- 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);
 
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+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);
+  }
+}
--- 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
    **/
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "ReceivedInstanceCallback.h"
+
+#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+#include "ICallbackRegistration.h"
+#include "PythonString.h"
+
+#include <Compatibility.h>  // 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<const char*>(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<OrthancPluginReceivedInstanceCallbackResult>(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_);
+}
--- /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 <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "PythonHeaderWrapper.h"
+
+void FinalizeReceivedInstanceCallback();
+
+PyObject* RegisterReceivedInstanceCallback(PyObject* module, PyObject* args);