Server-side scripting with Lua

Since release 0.5.2, Orthanc supports server-side scripting through the Lua scripting language. Thanks to this major feature, Orthanc can be tuned to specific medical workflows without being driven by an external script. This page summarizes the possibilities of Orthanc server-side scripting.

Many other examples are available in the source distribution.

A more expressive alternative to Lua scripts is provided by Python plugins and Java plugins.

Installing a Lua script

A custom Lua script can be installed either by the configuration file, or by uploading it through the REST API.

To install it by the configuration file method, you just have to specify the path to the file containing the Lua script in the LuaScripts variable. A comma-separated list of paths can be specified to install multiple scripts.

To upload a script stored in the file “script.lua” through the REST API, use the following command:

$ curl -X POST http://localhost:8042/tools/execute-script --data-binary @script.lua

Pay attention to the fact that, contrarily to the scripts installed from the configuration file, the scripts installed through the REST API are non-persistent: They are discarded after a restart of Orthanc, which makes them useful for script prototyping. You can also interpret a single Lua command through the REST API:

$ curl -X POST http://localhost:8042/tools/execute-script --data-binary "print(42)"

Note: The --data-binary cURL option is used instead of --data to prevent the interpretation of newlines by cURL, which is mandatory for the proper evaluation of the possible comments inside the Lua script.

Lua API

Callbacks to react to events

The Lua engine of Orthanc invokes the following callbacks that are triggered on various events. Here are the generic events:

  • function Initialize(): Invoked as soon as the Orthanc server is started.

  • function Finalize(): Invoked just before the Orthanc server is stopped.

Some permission-related events allow to filter incoming requests:

  • function ReceivedInstanceFilter(dicom, origin, info): Invoked to known whether an incoming DICOM instance should be accepted. See this section. The origin parameter is documented separately. The info parameter contains additional information and was added in Orthanc 1.6.1.

  • function IncomingHttpRequestFilter(method, uri, ip, username, httpHeaders): Invoked to known whether a REST request should be accepted. See this section.

Some job-related events allow to react to job completion/failure:

  • function OnJobSubmitted(jobId): Invoked when a new job has been submitted. Note that this does not mean the the job execution has started.

  • function OnJobFailure(jobId): Invoked when a job has failed.

  • function OnJobSuccess(jobId): Invoked when a job has completed successfully.

Some DICOM-related events allow to react to the reception of new medical images:

  • function OnStoredInstance(instanceId, tags, metadata, origin): Invoked whenever a new instance has been stored into Orthanc. This is especially useful for Auto-routing of DICOM images. The origin parameter is documented separately.

  • function OnStablePatient(patientId, tags, metadata): Invoked whenever a patient has not received any new instance for a certain amount of time (cf. stable resources and the option StableAge in the configuration file). The identifier of the patient is provided, together with her DICOM tags and her metadata.

  • function OnStableSeries(seriesId, tags, metadata): Invoked whenever a series has not received any new instance for a certain amount of time (cf. stable resources and the option StableAge in the configuration file).

  • function OnStableStudy(studyId, tags, metadata): Invoked whenever a study has not received any new instance for a certain amount of time (cf. stable resources and the option StableAge in the configuration file).

  • function IncomingFindRequestFilter(source, origin): Invoked whenever Orthanc receives an incoming C-Find query through the DICOM protocol. This allows to inspect the content of the C-Find query, and possibly modify it if a patch is needed for some manufacturer. A sample script is available.

Some other resource-related events are available:

  • function OnDeletedPatient(patientId): Invoked when a patient has been removed from the Orthanc database (new in Orthanc 1.6.0).

  • function OnDeletedStudy(studyId): Invoked when a study has been removed from the Orthanc database (new in Orthanc 1.6.0).

  • function OnDeletedSeries(seriesId): Invoked when a series has been removed from the Orthanc database (new in Orthanc 1.6.0).

  • function OnDeletedInstance(instanceId): Invoked when a instance has been removed from the Orthanc database (new in Orthanc 1.6.0).

  • function OnUpdatedPatient(patientId): Invoked when some metadata or some attachment associated with the given patient has been updated (new in Orthanc 1.6.0).

  • function OnUpdatedStudy(studyId): Invoked when some metadata or some attachment associated with the given study has been updated (new in Orthanc 1.6.0).

  • function OnUpdatedSeries(seriesId): Invoked when some metadata or some attachment associated with the given series has been updated (new in Orthanc 1.6.0).

  • function OnUpdatedInstance(instanceId): Invoked when some metadata or some attachment associated with the given instance has been updated (new in Orthanc 1.6.0).

Furthermore, in versions of Orthanc <= 1.8.2, whenever a DICOM association is negotiated for C-Store SCP, several callbacks are successively invoked to specify which transfer syntaxes are accepted for the association. These callbacks are listed in this sample script. These callbacks were removed in Orthanc 1.9.0.

If a callback is specified multiple times in separate scripts, the event handler of the latest loaded script is used.

Concurrency and deadlocks

Orthanc only implements a single Lua context. Therefore, all these callbacks are guaranteed to be invoked in mutual exclusion. This implies that Lua scripting in Orthanc does not support any kind of concurrency but may also lead to some deadlocks.

If a lua function (e.g. OnHeartBeat) performs an HTTP call to an external Rest API (e.g. http://myserver.com/orthanc_is_alive.php) which, in turn, calls the Orthanc Rest API (e.g. call http://orthanc:8042/system), odds are high that you meet a deadlock because Orthanc, when handling a Rest API calls, may try to execute some Lua callbacks (e.g. IncomingHttpRequestFilter) while the Lua context is still blocked inside the OnHeartBeat function.

To avoid deadlocks, always make sure to avoid such back-and-forth communications or make sure they happen asynchronously: your webservice should call the Orthanc Rest API after it has returned from the endpoint called by OnHeartBeat.

Note that these deadlocks won’t happen when a lua function calls its own Orthanc Rest API using the RestApiGet, RestApiPost, … functions.

Calling the REST API of Orthanc

Lua scripts have full access to the REST API of Orthanc through the following functions:

  • RestApiGet(uri, builtin, headers)

  • RestApiPost(uri, body, builtin, headers)

  • RestApiPut(uri, body, builtin, headers)

  • RestApiDelete(uri, builtin, headers)

Here is a description of the parameters:

  • uri specifies the resource being accessed (e.g. /instances). It must not include the URL schema (protocol), hostname or port.

  • In the context of a POST or PUT request, body is a string containing the body of the request (e.g. {"Keep":"StudyDate"}). This string will often correspond to a JSON-formatted version of a Lua table. The DumpJson() function (see below) is very useful to achieve this conversion from a Lua table to a plain string.

  • builtin is an optional Boolean that specifies whether the request targets only the built-in REST API of Orthanc (if set to true), or the full the REST API after being tainted by plugins (if set to false).

  • headers is an optional argument and was added in release 1.5.7. It allows to provide the REST API endpoint with HTTP headers.

For instance:

RestApiPost('/instances/5af318ac-78fb-47ff-b0b0-0df18b0588e0/anonymize', '{}')

Instance modification/routing

The Lua engine offers the following special functions to modify and route DICOM instances:

  • ModifyInstance(instanceId, replacements, removals, removePrivateTags) modifies an instance.

  • SendToModality(instanceId, modality) performs a C-Store to the target modality.

  • SendToPeer(instanceId, peer) sends the instance to a remote Orthanc peer.

  • Delete(instanceId) deletes the instance.

See this section for examples. As can be seen in those examples, these special functions can be chained together, although they return no explicit value.

Note that these special functions should only be used for basic use cases: Calls to the REST API should always be favored for auto-routing.

General-purpose functions

The Lua engine of Orthanc contain several general-purpose ancillary functions:

  • PrintRecursive(v) recursively prints the content of a Lua table to the log file of Orthanc.

  • ParseJson(s) converts a string encoded in the JSON format to a Lua table.

  • DumpJson(v, keepStrings) encodes a Lua table as a JSON string. Setting the optional argument keepStrings (available from release 0.9.5) to true prevents the automatic conversion of strings to integers.

  • GetOrthancConfiguration() returns a Lua table containing the content of the configuration files of Orthanc.

Similarly to the functions to call the REST API of Orthanc, several functions are available to make generic HTTP requests to Web services:

  • HttpGet(url, headers)

  • HttpPost(url, body, headers)

  • HttpPut(url, body, headers)

  • HttpDelete(url, headers)

  • SetHttpCredentials(username, password) can be used to setup the HTTP credentials.

  • SetHttpTimeout(timeout) can be used to configure a timeout (in seconds). When contacting an external webservice, it is recommended to configure a very short timeout not to lock the Lua context for too long. No other Lua callbacks may be run at the same time which may have a significant impact on Orthanc responsivness in general. This function has been introduced in version 1.11.1.

The headers argument is optional and was added in release 1.2.1. It allows to set the HTTP headers for the HTTP client request.

Example:

local preview = RestApiGet('/instances/' .. instanceId .. '/preview')
local headers = {
   ["content-type"] = "image/png",
}

SetHttpCredentials('user', 'pwd')
SetHttpTimeout(1)
HttpPost("http://localhost/my-web-service/instance-preview", preview, headers)

Origin of the instances

Whenever Orthanc decides whether it should should store a new instance (cf. the ReceivedInstanceFilter() callback), or whenever it has actually stored a new instance (cf. the OnStoredInstance callback), an origin parameter is provided. This parameter is a Lua table that describes from which Orthanc subsystem the new instance comes from.

There are 4 possible subsystems, that can be distinguished according to the value of origin["RequestOrigin"]:

  • RestApi: The instance originates from some HTTP request to the REST API. In this case, the RemoteIp and Username fields are available in origin. They respectively describe the IP address of the HTTP client, and the username that was used for HTTP authentication (as defined in the RegisteredUsers configuration variable).

  • DicomProtocol: The instance originates from a DICOM C-Store. The fields RemoteIp, RemoteAet and CalledAet respectively provide the IP address of the DICOM SCU (client), the application entity title of the DICOM SCU client, and the application entity title of the Orthanc SCP server. The CalledAet can be used for advanced auto-routing scenarios, when a single instance of Orthanc acts as a proxy for several DICOM SCU clients.

  • Lua: The instance originates from a Lua script.

  • Plugins: The instance originates from a plugin.

Filtering incoming DICOM instances

Each time a DICOM instance is received by Orthanc (either through the DICOM protocol or through the REST API), the ReceivedInstanceFilter() Lua function is invoked. If this callback returns true, the instance is accepted for storage. If it returns false, the instance is discarded. This mechanism can be used to filter the incoming DICOM instances. Here is an example of a Lua filter that only allows incoming instances of MR modality:

function ReceivedInstanceFilter(dicom, origin, info)
   -- Only allow incoming MR images
   if dicom.Modality == 'MR' then
      return true
   else
      return false
   end
end

The argument dicom corresponds to a Lua table (i.e. an associative array) that contains the DICOM tags of the incoming instance. For debugging purpose, you can print this structure as follows:

function ReceivedInstanceFilter(dicom, origin, info)
   PrintRecursive(dicom)
   -- Accept all incoming instances (default behavior)
   return true
end

The argument origin is documented separately.

The argument info was introduced in Orthanc 1.6.1. It contains some additional information about the received DICOM instance, notably:

  • HasPixelData is true iff. the Pixel Data (7FE0,0010) tag is present.

  • TransferSyntaxUID contains the transfer syntax UID of the dataset of the instance (if applicable).

Filtering incoming REST requests

Lua scripting can be used to control the access to the various URI of the REST API. Each time an incoming HTTP request is received, the IncomingHttpRequestFilter() Lua function is called. The access to the resource is granted if and only if this callback script returns true.

This mechanism can be used to implement fine-grained access control lists. Here is an example of a Lua script that limits POST, PUT and DELETE requests to an user that is called “admin”:

function IncomingHttpRequestFilter(method, uri, ip, username, httpHeaders)
   -- Only allow GET requests for non-admin users

  if method == 'GET' then
      return true
   elseif username == 'admin' then
      return true
   else
      return false
   end
end

Here is a description of the arguments of this Lua callback:

  • method: The HTTP method (GET, POST, PUT or DELETE).

  • uri: The path to the resource (e.g. /tools/generate-uid).

  • ip: The IP address of the host that has issued the HTTP request (e.g. 127.0.0.1).

  • username: If HTTP Basic Authentication is enabled in the configuration file, the name of the user that has issued the HTTP request (as defined in the RegisteredUsers configuration variable). If the authentication is disabled, this argument is set to the empty string.

  • httpHeaders: The HTTP headers of the incoming request. This argument is available since Orthanc 1.0.1. It is useful if the authentication should be achieved through tokens, for instance against a LDAP or OAuth2 server.

Auto-routing of DICOM images

Since release 0.8.0, the routing of DICOM flows can be very easily automated with Orthanc. All you have to do is to declare your destination modality in the configuration file (section DicomModalities), then to create and install a Lua script. For instance, here is a sample script:

function OnStoredInstance(instanceId, tags, metadata)
  Delete(SendToModality(instanceId, 'sample'))
end

If this script is loaded into Orthanc, whenever a new DICOM instance is received by Orthanc, it will be routed to the modality whose symbolic name is sample (through a Store-SCU command), then it will be removed from Orthanc. In other words, this is a one-liner script to implement DICOM auto-routing.

Very importantly, thanks to this feature, you do not have to use the REST API or to create external scripts in order to automate simple imaging flows. The scripting engine is entirely contained inside the Orthanc core system.

Thanks to Lua expressiveness, you can also implement conditional auto-routing. For instance, if you wish to route only patients whose name contains “David”, you would simply write:

function OnStoredInstance(instanceId, tags, metadata)
   -- Extract the value of the "PatientName" DICOM tag
   local patientName = string.lower(tags['PatientName'])

  if string.find(patientName, 'david') ~= nil then
      -- Only route patients whose name contains "David"
      Delete(SendToModality(instanceId, 'sample'))

  else
      -- Delete the patients that are not called "David"
      Delete(instanceId)
   end
end

Besides SendToModality(), a mostly identical function with the same arguments called SendToPeer() can be used to route instances to Orthanc peers. It is also possible to modify the received instances before routing them. For instance, here is how you would replace the StationName DICOM tag:

function OnStoredInstance(instanceId, tags, metadata)
   -- Ignore the instances that result from a modification to avoid
   -- infinite loops
   if (metadata['ModifiedFrom'] == nil and
       metadata['AnonymizedFrom'] == nil) then

     -- The tags to be replaced
      local replace = {}
      replace['StationName'] = 'My Medical Device'

     -- The tags to be removed
      local remove = { 'MilitaryRank' }

     -- Modify the instance, send it, then delete the modified instance
      Delete(SendToModality(ModifyInstance(instanceId, replace, remove, true), 'sample'))

     -- Delete the original instance
      Delete(instanceId)
   end
end

Important remarks about auto-routing

The SendToModality(), SendToPeer(), ModifyInstance() and Delete() functions are for the most basic cases of auto-routing (implying a single DICOM instance, and possibly a basic modification of this instance). The ModifyInstance() function could also lead to problems if it deals with tags wrongly interpreted as numbers by Lua.

For more evolved auto-routing scenarios, remember that Lua scripts have full access to the REST API of Orthanc. This is illustrated by the AutoroutingModification.lua sample available in the source distribution of Orthanc:

function OnStoredInstance(instanceId, tags, metadata, origin)
   -- Ignore the instances that result from the present Lua script to
   -- avoid infinite loops
   if origin['RequestOrigin'] ~= 'Lua' then

      -- The tags to be replaced
      local replace = {}
      replace['StationName'] = 'My Medical Device'
      replace['0031-1020'] = 'Some private tag'

      -- The tags to be removed
      local remove = { 'MilitaryRank' }

      -- Modify the instance
      local command = {}
      command['Replace'] = replace
      command['Remove'] = remove
      local modifiedFile = RestApiPost('/instances/' .. instanceId .. '/modify', DumpJson(command, true))

      -- Upload the modified instance to the Orthanc database so that
      -- it can be sent by Orthanc to other modalities
      local modifiedId = ParseJson(RestApiPost('/instances/', modifiedFile)) ['ID']

      -- Send the modified instance to another modality
      RestApiPost('/modalities/sample/store', modifiedId)

      -- Delete the original and the modified instances
      RestApiDelete('/instances/' .. instanceId)
      RestApiDelete('/instances/' .. modifiedId)
   end
end

Also note that other callbacks are available (OnStablePatient(), OnStableStudy() and OnStableSeries()) to react to other events than the reception of a single instance with OnStoredInstance().

Fixing C-Find requests

C-Find requests are sometimes interpreted differently by different DICOM servers (e.g. the * wildcard, as reported by users), and sometimes a querying modality might set unexpected DICOM tags (cf. this real-world example). In such situations, it is possible to dynamically fix incoming or outgoing C-Find queries using a Lua script.

Sanitizing incoming C-Find requests can be done by implementing the IncomingFindRequestFilter(query, origin) callback that is called whenever the Orthanc C-Find SCP is queried by a remote modality. For instance, here is Lua script to remove a private tag that is specified by some manufacturer:

function IncomingFindRequestFilter(query, origin)
  -- First display the content of the C-Find query
  PrintRecursive(query)
  PrintRecursive(origin)

  -- Remove the "PrivateCreator" tag from the query
  local v = query
  v['5555,0010'] = nil

  return v
end

The origin argument contains information about which modality has issued the request.

Note that this callback allows you to modify the incoming request but will not modify the list of tags that Orthanc will return.

Also note that the IncomingFindRequestFilter callback is not applied to C-Find requests targeting modality worklists. Since Orthanc 1.4.2, the corresponding IncomingWorklistRequestFilter callback can be used to sanitize C-FIND requests against worklists:

function IncomingWorklistRequestFilter(query, origin)
  PrintRecursive(query)
  PrintRecursive(origin)

  -- Implements the same behavior as the "FilterIssuerAet"
  -- option of the sample worklist plugin
  query['0040,0100'][1]['0040,0001'] = origin['RemoteAet']

  return query
end

Similarly, the callback OutgoingFindRequestFilter(query, modality) is invoked whenever Orthanc acts as a C-Find SCU, which gives the opportunity to dynamically fix outgoing C-Find requests before they are actually sent to the queried modality. For instance, here is a sample Lua callback that would replace asterisk wildcards (i.e. *) by an empty string for any query/retrieve issued by Orthanc (including from Orthanc Explorer):

function OutgoingFindRequestFilter(query, modality)
  for key, value in pairs(query) do
    if value == '*' then
      query[key] = ''
    end
  end

  return query
end

HeartBeat

Starting from Orthanc 1.11.1, one can run a Lua callback at regular interval. This interval is defined in the LuaHeartBeatPeriod configuration:

function OnHeartBeat()

  -- ping a webservice to notify that Orthanc is still alive
  SetHttpCredentials('user', 'pwd')
  SetHttpTimeout(1)
  HttpPost("http://localhost/my-web-service/still-alive", "my-id", {})

end

Using external modules

Starting with Orthanc 1.3.2, it is possible to use external Lua modules if Orthanc was compiled with the -DENABLE_LUA_MODULES=ON while invoking CMake.

Importantly, the modules and the Orthanc server must use the same version of Lua for external modules to be properly loaded.

Check out the Orthanc Users forum for old discussions about this topic: reference 1, reference 2, reference 3.