# HG changeset patch # User Sebastien Jodogne # Date 1466769026 -7200 # Node ID 66ff02eefca6f4081b1cf3a80dc3ab755b30e9f3 # Parent 36be09e49ad153717e615128191f1684310c9c98 Usage.txt diff -r 36be09e49ad1 -r 66ff02eefca6 Plugin/Configuration.cpp --- a/Plugin/Configuration.cpp Fri Jun 24 12:06:03 2016 +0200 +++ b/Plugin/Configuration.cpp Fri Jun 24 13:50:26 2016 +0200 @@ -395,6 +395,13 @@ } + unsigned int GetUnsignedIntegerValue(const std::string& key, + unsigned int defaultValue) + { + return configuration_.GetUnsignedIntegerValue(key, defaultValue); + } + + std::string GetRoot() { std::string root = configuration_.GetStringValue("Root", "/dicom-web/"); diff -r 36be09e49ad1 -r 66ff02eefca6 Plugin/Configuration.h --- a/Plugin/Configuration.h Fri Jun 24 12:06:03 2016 +0200 +++ b/Plugin/Configuration.h Fri Jun 24 13:50:26 2016 +0200 @@ -92,6 +92,9 @@ bool GetBooleanValue(const std::string& key, bool defaultValue); + unsigned int GetUnsignedIntegerValue(const std::string& key, + unsigned int defaultValue); + std::string GetRoot(); std::string GetWadoRoot(); diff -r 36be09e49ad1 -r 66ff02eefca6 Plugin/DicomWebClient.cpp --- a/Plugin/DicomWebClient.cpp Fri Jun 24 12:06:03 2016 +0200 +++ b/Plugin/DicomWebClient.cpp Fri Jun 24 13:50:26 2016 +0200 @@ -168,9 +168,12 @@ size_t& countInstances, bool force) { + unsigned int maxInstances = OrthancPlugins::Configuration::GetUnsignedIntegerValue("StowMaxInstances", 10); + size_t maxSize = static_cast(OrthancPlugins::Configuration::GetUnsignedIntegerValue("StowMaxSize", 10)) * 1024 * 1024; + if ((force && countInstances > 0) || - countInstances > 10 /* TODO Parameter */ || - chunks.GetNumBytes() > 10 * 1024 * 1024 /* TODO Parameter */) + (maxInstances != 0 && countInstances >= maxInstances) || + (maxSize != 0 && chunks.GetNumBytes() >= maxSize)) { chunks.AddChunk("\r\n--" + boundary + "--\r\n"); @@ -441,10 +444,10 @@ std::string uri = "studies/" + study; if (!series.empty()) { - uri += "/" + series; + uri += "/series/" + series; if (!instance.empty()) { - uri += "/" + instance; + uri += "/instances/" + instance; } } diff -r 36be09e49ad1 -r 66ff02eefca6 README --- a/README Fri Jun 24 12:06:03 2016 +0200 +++ b/README Fri Jun 24 13:50:26 2016 +0200 @@ -32,17 +32,12 @@ the "./Status.txt" file. -Install -------- +Installation and usage +---------------------- Build instructions can be found in "./Resources/BuildInstructions.txt". - -Samples -------- - -Python samples to call the DICOMweb services can be found in the -"./Samples" folder. +Usage instructions can be found in "./Usage.txt". Licensing: AGPL diff -r 36be09e49ad1 -r 66ff02eefca6 Resources/Samples/Python/SendStow.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Python/SendStow.py Fri Jun 24 13:50:26 2016 +0200 @@ -0,0 +1,77 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, 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 . + + + +# We do not use Python's "email" package, as it uses LF (\n) for line +# endings instead of CRLF (\r\n) for binary messages, as required by +# RFC 1341 +# http://stackoverflow.com/questions/3086860/how-do-i-generate-a-multipart-mime-message-with-correct-crlf-in-python +# https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + +import requests +import sys +import json +import uuid + +if len(sys.argv) < 2: + print('Usage: %s ...' % sys.argv[0]) + print('') + print('Example: %s http://localhost:8042/dicom-web/studies hello.dcm world.dcm' % sys.argv[0]) + sys.exit(-1) + +URL = sys.argv[1] + +# Create a multipart message whose body contains all the input DICOM files +boundary = str(uuid.uuid4()) # The boundary is a random UUID +body = bytearray() + +for i in range(2, len(sys.argv)): + try: + with open(sys.argv[i], 'rb') as f: + body += bytearray('--%s\r\n' % boundary, 'ascii') + body += bytearray('Content-Type: application/dicom\r\n\r\n', 'ascii') + body += f.read() + body += bytearray('\r\n', 'ascii') + except: + print('Ignoring directory %s' % sys.argv[i]) + +# Closing boundary +body += bytearray('--%s--' % boundary, 'ascii') + +# Do the HTTP POST request to the STOW-RS server +r = requests.post(URL, data=body, headers= { + 'Content-Type' : 'multipart/related; type=application/dicom; boundary=%s' % boundary, + 'Accept' : 'application/json', +}) + +j = json.loads(r.text) + +# Loop over the successful instances +print('\nWADO-RS URL of the uploaded instances:') +for instance in j['00081199']['Value']: + if '00081190' in instance: # This instance has not been discarded + url = instance['00081190']['Value'][0] + print(url) + +print('\nWADO-RS URL of the study:') +try: + print(j['00081190']['Value'][0]) +except: + print('No instance was uploaded!') diff -r 36be09e49ad1 -r 66ff02eefca6 Resources/Samples/Python/WadoRetrieveStudy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/Samples/Python/WadoRetrieveStudy.py Fri Jun 24 13:50:26 2016 +0200 @@ -0,0 +1,42 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, 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 . + + +import email +import urllib2 +import sys + +if len(sys.argv) != 2: + print('Usage: %s ' % sys.argv[0]) + print('') + print('Example: %s http://localhost:8042/dicom-web/studies/1.3.51.0.1.1.192.168.29.133.1681753.1681732' % sys.argv[0]) + sys.exit(-1) + +answer = urllib2.urlopen(sys.argv[1]) +s = str(answer.info()) + "\n" + answer.read() + +msg = email.message_from_string(s) + +for i, part in enumerate(msg.walk(), 1): + filename = 'wado-%06d.dcm' % i + dicom = part.get_payload(decode = True) + if dicom != None: + print('Storing DICOM file: %s' % filename) + with open(filename, 'wb') as f: + f.write(str(dicom)) diff -r 36be09e49ad1 -r 66ff02eefca6 Resources/Samples/SendStow.py --- a/Resources/Samples/SendStow.py Fri Jun 24 12:06:03 2016 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -#!/usr/bin/python - -# Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics -# Department, University Hospital of Liege, 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 . - - - -# We do not use Python's "email" package, as it uses LF (\n) for line -# endings instead of CRLF (\r\n) for binary messages, as required by -# RFC 1341 -# http://stackoverflow.com/questions/3086860/how-do-i-generate-a-multipart-mime-message-with-correct-crlf-in-python -# https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html - -import requests -import sys -import json -import uuid - -if len(sys.argv) < 2: - print('Usage: %s ...' % sys.argv[0]) - print('') - print('Example: %s http://localhost:8042/dicom-web/studies hello.dcm world.dcm' % sys.argv[0]) - sys.exit(-1) - -URL = sys.argv[1] - -# Create a multipart message whose body contains all the input DICOM files -boundary = str(uuid.uuid4()) # The boundary is a random UUID -body = bytearray() - -for i in range(2, len(sys.argv)): - try: - with open(sys.argv[i], 'rb') as f: - body += bytearray('--%s\r\n' % boundary, 'ascii') - body += bytearray('Content-Type: application/dicom\r\n\r\n', 'ascii') - body += f.read() - body += bytearray('\r\n', 'ascii') - except: - print('Ignoring directory %s' % sys.argv[i]) - -# Closing boundary -body += bytearray('--%s--' % boundary, 'ascii') - -# Do the HTTP POST request to the STOW-RS server -r = requests.post(URL, data=body, headers= { - 'Content-Type' : 'multipart/related; type=application/dicom; boundary=%s' % boundary, - 'Accept' : 'application/json', -}) - -j = json.loads(r.text) - -# Loop over the successful instances -print('\nWADO-RS URL of the uploaded instances:') -for instance in j['00081199']['Value']: - if '00081190' in instance: # This instance has not been discarded - url = instance['00081190']['Value'][0] - print(url) - -print('\nWADO-RS URL of the study:') -try: - print(j['00081190']['Value'][0]) -except: - print('No instance was uploaded!') diff -r 36be09e49ad1 -r 66ff02eefca6 Resources/Samples/WadoRetrieveStudy.py --- a/Resources/Samples/WadoRetrieveStudy.py Fri Jun 24 12:06:03 2016 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -#!/usr/bin/python - -# Orthanc - A Lightweight, RESTful DICOM Store -# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics -# Department, University Hospital of Liege, 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 . - - -import email -import urllib2 -import sys - -if len(sys.argv) != 2: - print('Usage: %s ' % sys.argv[0]) - print('') - print('Example: %s http://localhost:8042/dicom-web/studies/1.3.51.0.1.1.192.168.29.133.1681753.1681732' % sys.argv[0]) - sys.exit(-1) - -answer = urllib2.urlopen(sys.argv[1]) -s = str(answer.info()) + "\n" + answer.read() - -msg = email.message_from_string(s) - -for i, part in enumerate(msg.walk(), 1): - filename = 'wado-%06d.dcm' % i - dicom = part.get_payload(decode = True) - if dicom != None: - print('Storing DICOM file: %s' % filename) - with open(filename, 'wb') as f: - f.write(str(dicom)) diff -r 36be09e49ad1 -r 66ff02eefca6 Usage.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Usage.txt Fri Jun 24 13:50:26 2016 +0200 @@ -0,0 +1,275 @@ +============= +Configuration +============= + +(1) You must change the Orthanc configuration file to tell Orthanc + where it can find the DICOMweb plugin. This is done by properly + modifying the "Plugins" configuration option of Orthanc. For + instance, in Linux: + +{ + ... + "Plugins" : [ + "/home/user/OrthancDicomWeb/Build/libOrthancDicomWeb.so" + ] + ... +} + + Or in Windows: + +{ + ... + "Plugins" : [ + "c:/Temp/OrthancDicomWeb.dll" + ] + ... +} + + Note that the DICOMweb server will share all the parameters of the + Orthanc HTTP server, notably wrt. authentication and HTTPS + encryption. For this reason, you will most probably have to enable + the remote access to the Orthanc HTTP server: + +{ + ... + "RemoteAccessEnabled" : true + ... +} + + +(2) There are several configuration options that can be set to + fine-tune the Orthanc DICOMweb server. Here is the full list of + the available options, all of them must be grouped inside the + "DicomWeb" section of the Orthanc configuration file: + +{ + ... + "DicomWeb" : { + "Enable" : true, // Whether DICOMweb support is enabled + "Root" : "/dicom-web/", // Root URI of the DICOMweb API (for QIDO-RS, STOW-RS and WADO-RS) + "EnableWado" : true, // Whether WADO-URI (previously known as WADO) support is enabled + "WadoRoot" : "/wado", // Root URI of the WADO-URI (aka. WADO) API + "Host" : "localhost", // Hard-codes the name of the host for subsequent WADO-RS requests + "Ssl" : false, // Whether HTTPS should be used for subsequent WADO-RS requests + "StowMaxInstances" : 10, // For STOW-RS client, the maximum number of instances in one single HTTP query (0 = no limit) + "StowMaxSize" : 10 // For STOW-RS client, the maximum size of the body in one single HTTP query (in MB, 0 = no limit) + } + ... +} + + +(3) If you want to connect Orthanc as a client to remote DICOMweb + servers (cf. below), you need to modify the configuration file so + as to define each of them in the option "DicomWeb.Servers". The + syntax is identical to the "OrthancPeers" parameters. + + In the most simple case, here is how to instruct Orthanc about the + existence of a password-less DICOMweb server: + +{ + ... + "DicomWeb" : { + "Servers" : [ "http://192.168.1.1/dicom-web/" ] + } + ... +} + + If the DICOMweb server is protected by a password (with HTTP Basic + access authentication): + +{ + ... + "DicomWeb" : { + "Servers" : [ "http://192.168.1.1/dicom-web/", "username", "password" ] + } + ... +} + + If the DICOMweb server is protected with HTTPS client + authentication, you must provide your client certificate (in the + PEM format), your client private key (in the PEM format), together + with the password protecting the private key: + +{ + ... + "DicomWeb" : { + "Servers" : [ + { + "Url" : "http://192.168.1.1/dicom-web/", + "CertificateFile" : "client.crt", + "CertificateKeyFile" : "client.key", + "CertificateKeyPassword" : "password" + } + ] + } + ... +} + + Finally, it is also possible to use client authentication with + hardware security modules and smart cards through PKCS#11: + +{ + ... + "DicomWeb" : { + "Servers" : [ + { + "Url" : "http://192.168.1.1/dicom-web/", + "Pkcs11" : true + } + ] + } + ... +} + + Important remark: When querying a DICOMweb server, Orthanc will + automatically use the global configuration options "HttpProxy", + "HttpTimeout", "HttpsVerifyPeers", "HttpsCACertificates", and + "Pkcs11". Make sure to adapt them if need be. + + + +================================= +Querying a remote DICOMweb server +================================= + +Listing the available servers +----------------------------- + +The list of the remote DICOMweb servers that are known to the DICOMweb +plugin can be obtained as follows: + +# curl http://localhost:8042/dicom-web/servers/ +[ "sample" ] + +Here, a single server called "sample" is configured. + + +Making a call to QIDO-RS or WADO-RS +----------------------------------- + +In Orthanc, the URI "/{dicom-web}/servers/{name}/get" allows to make a +HTTP GET call against a DICOMweb server. This can be used to issue a +QIDO-RS or WADO-RS command. Orthanc will take care of properly +encoding the URL and authenticating the client. + +For instance, here is a sample QIDO-RS search to query all the +studies (using a bash command-line): + +# curl http://localhost:8042/dicom-web/servers/sample/get -d @- << EOF +{ + "Uri" : "/studies" +} +EOF + +You do not have to specify the base URL of the remote DICOMweb server, +as it is encoded in the configuration file. + +The result of the command above is a multipart "application/dicom+xml" +document. It is possible to request a more human-friendly JSON answer +by adding the "Accept" HTTP header. Here is how to search for a given +patient name, while requesting a JSON answer and pretty-printing +through the "json_pp" command-line tool: + +# curl http://localhost:8042/dicom-web/servers/sample/get -d @- << EOF | json_pp +{ + "Uri" : "/studies", + "HttpHeaders" : { + "Accept" : "application/json" + }, + "Arguments" : { + "00100010" : "*JODOGNE*" + } +} +EOF + +Note how all the GET arguments must be specified in the "Arguments" +field. Orthanc will take care of properly encoding it to a URL. + +An user-friendly reference of the features available in QIDO-RS and +WADO-RS can be found at http://dicomweb.hcintegrations.ca/#/home + + +Sending DICOM resources to a STOW-RS server +------------------------------------------- + +STOW-RS allows to send local DICOM resources to a remote DICOMweb +server. In Orthanc, the STOW-RS client primitive is available at URI +"/{dicom-web}/servers/{name}/stow". Here is a sample call: + +# curl http://localhost:8042/dicom-web/servers/sample/stow -X POST -d @- << EOF +{ + "Resources" : [ + "6ca4c9f3-5e895cb3-4d82c6da-09e060fe-9c59f228" + ] +} +EOF + +Note that this primitive takes as its input a list of Orthanc +identifiers corresponding to the resources (patients, studies, series +and/or instances) to be exported: +https://orthanc.chu.ulg.ac.be/book/faq/orthanc-ids.html + +Remark 1: Additional HTTP headers can be added with an optional +"HttpHeaders" argument, as for QIDO-RS and WADO-RS. This might be +useful e.g. for cookie-based session management. + +Remark 2: One call to this "/stow" primitive will possibly result in +several HTTP requests to the DICOMweb server, in order to limit the +size of the HTTP messages. The configuration options +"DicomWeb.StowMaxInstances" and "DicomWeb.StowMaxSize" can be used to +tune this behavior (set both options to 0 to send one single request). + + +Retrieving DICOM resources from a WADO-RS server +------------------------------------------------ + +Once DICOM resources of interest have been identified through a +QIDO-RS call to a remote DICOMweb server (cf. above), it is +interesting to download them locally with a WADO-RS call. You could do +it manually with a second call to the +"/{dicom-web}/servers/{name}/get" URI, but Orthanc provides another +primitive "/retrieve" to automate this process. + +Here is how you would download one study, one series and one instance +whose StudyInstanceUID (0020,000d), SeriesInstanceUID (0020,000e) are +SOPInstanceUID (0008,0018) have been identified through a former +QIDO-RS call: + +# curl http://localhost:8042/dicom-web/servers/sample/retrieve -X POST -d @- << EOF +{ + "Resources" : [ + { + "Study" : "1.3.51.0.1.1.192.168.29.133.1688840.1688819" + }, + { + "Study" : "1.3.51.0.1.1.192.168.29.133.1681753.1681732", + "Series" : "1.3.12.2.1107.5.2.33.37097.2012041613040617636372171.0.0.0" + }, + { + "Study" : "1.3.51.0.1.1.192.168.29.133.1681753.1681732", + "Series" : "1.3.12.2.1107.5.2.33.37097.2012041612474981424569674.0.0.0", + "Instance" : "1.3.12.2.1107.5.2.33.37097.2012041612485540185869716" + } + ] +} +EOF + +Orthanc will reply with the list of the Orthanc identifiers of all the +DICOM instances that were downloaded from the remote server. + +Remark 1: Contrarily to the "/stow" URI that uses Orthanc identifiers, +the "/retrieve" URI uses DICOM identifiers. + +Remark 2: The "HttpArguments" is also available. + + + +======= +Samples +======= + +Samples of how to call DICOMweb services from standalone applications +can be found in the following folders: + +- In Python: see ./Resources/Samples/Python/ +- In JavaScript: see ./Resources/Samples/Python/