Server-side scripting with Lua¶
Contents
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. Theorigin
parameter is documented separately. Theinfo
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. Theorigin
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 optionStableAge
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 optionStableAge
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 optionStableAge
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. TheDumpJson()
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 totrue
), or the full the REST API after being tainted by plugins (if set tofalse
).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 synchronous C-Store to the target modality.SendToPeer(instanceId, peer)
sends the instance to a remote Orthanc peer synchronously.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 argumentkeepStrings
(available from release 0.9.5) totrue
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, theRemoteIp
andUsername
fields are available inorigin
. They respectively describe the IP address of the HTTP client, and the username that was used for HTTP authentication (as defined in theRegisteredUsers
configuration variable).DicomProtocol
: The instance originates from a DICOM C-Store. The fieldsRemoteIp
,RemoteAet
andCalledAet
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. TheCalledAet
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
istrue
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 theRegisteredUsers
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.