1. Getting Started

A good way to get started is to have a look at the examples included in the distribution.

We provide email support during the 30-day trial period of your evaluation. If you need help during your evaluation or have technical questions, please contact support via email at support@trispark.com.

1.1. Installation

If you have downloaded an installer distribution, run the installer. If you have downloaded JDT as a zip file, unzip the file into a directory of your choice.

1.1.1. Jar File

The JDT library is contained within a single jar file named jdt.jar and located in the lib folder of installation directory. You will need to add this jar file to the classpath of your development environment. When applications that make use of JDT are distributed, this jar file should also be distributed together with the jar files and other files of your applications.

1.1.2. Key File

JDT requires a key file called jdt.key in order to run. The key file can be an evaluation key which you received by email or it can be a permanent key which you received after purchasing a license. The key should be placed in the classpath of your development environment and it should also be redistributed together with the jdt.jar file when you distribute the applications you have developed with JDT.

We provide email support during the 30-day trial period of your evaluation. If you need help during your evaluation or have technical questions, please contact support via email at support@trispark.com.

1.2. Features

The Trispark Java DICOM Toolkit is a small footprint 100% java library and API for building DICOM applications. The library has the following features:

  • Up-to-date DICOM dictionary classes covering all tags,transfer syntaxes, sop classes and other UID’s defined in the 2008 edition of the DICOM standard

  • DICOM PART 10 File support including automatic detection of DICOM files

  • DICOM PART 8 Network support for creating 'associations' over TCP/IP and connecting to DICOM devices, including support for SCP/SCU role negotiation, maximum operations setting, extended negotiations and easy exchange of DIMSE commands and Data Sets over associations.

  • Support for all non-compressed transfer syntaxes and transfer syntax conversion when reading/writing DICOM files and streams.

  • All VR’s are supported including newly added OF (Other Float).

  • Sequences can be read or written with defined or undefined length.

  • Sequence delimitation items and Item delimitation items are added automatically when necessary.

  • Support for RLE encoding and JPEG encoding

  • Read/Write DICOM data to InputStreams/OutputStreams using the normal java.io streams concepts.

  • Easy manipulation of sequences and positioning of sequence items with utility classes

1.3. Support

Customers having purchased a distribution license and/or source code license are entitled to one year of technical support via email starting from the purchase date. The support also includes minor updates and bug fix releases.Support can be renewed annually.

If you have any technical questions contact support@trispark.com.

1.4. Additional Resources

This software library requires a basic understanding of the DICOM standard. The DICOM standard is quite an extensive standard published by National Electrical Manufacturers Association 1300 N. 17th Street Rosslyn, Virginia 22209 USA

The official ACR/NEMA home page of the DICOM standard is medical.nema.org.

A very good site on DICOM with links to the various parts of the standard, correction proposals and supplements in PDF format is David Clunie’s Medical Imaging Format site.

The parts of the DICOM standard that are particularly relevant with respect to this library are:

  • Part 5: Data Structures and Encoding

  • Part 7: Message Exchange

  • Part 8: Network Communication Support for Message Exchange

  • Part 10: Media Storage and File Format for Data Interchange

2. Working with DICOM files and Datasets

2.1. DICOM File Format

The DICOM File Format is described in Part 10 of the DICOM Standard: "Media Storage and File Format for Data Interchange". A DICOM file consists of a file header followed by a File Meta Information Data Set and a Data Set representing a single SOP Instance.

2.1.1. File Header

The File Header is made up of a 128-byte File Preamble followed by a 4-byte prefix. The prefix consists of the uppercase characters 'D','I','C','M'. The DICOM standard does not define how the 128-byte File Preamble should be structured. The purpose of the File Preamble is to provide a way for a DICOM file to be compatible with a number of common image file formats. For example the File Preamble my contain offset information that allows an application to have direct access to the image data without having to parse the complete DICOM file. If the File Preamble is not used, all bytes must be zero. Note: the file header is normally a required part of a DICOM file, but sometimes only the dataset following the file header is stored physically on disk. This may for example be the case in the internal storage of PACS systems. JDT is also capable of reading and writing data sets stored as files without a file header.

2.1.2. File Meta Information

The File Meta Information follows the File Header and has the structure of a Data Set with Data Elements. All Data Elements have group number 0x0002 and contain specific information about the DICOM file. The Transfer Syntax of the File Meta Information Data set is always Explicit Little Endian. The Transfer Syntax of the Data Set following the File Meta Information is encoded as a Data Element within the File Meta Information. JDT reads and writes File Meta Information and stores it in a DicomObject accessible from the DicomObject instance that contains the Data Set of the rest of the DICOM file.

2.1.3. Data Set

The Data Set is stored in the portion of the file following the file header. A data set represents a single SOP instance of a particular SOP class. The Data Set contains public Data Elements identified by a tag with an even group number and it may also contain private Data Elements identified by a tag with an odd group number.

2.1.4. Data Elements

Data Elements are the individual units of information within a DICOM file. Data Elements may be nested. This means the value of a Data Element may contain a number of Data Sets. Data Elements that contain nested Data Sets are called Sequences and a single Data Set that is nested within a Sequence is called a Sequence Item.

2.2. Reading DICOM Files and Data Sets

DICOM files and raw Data Sets (without the 128 byte preamble and group 0x0002) can be read from arbitrary inputstreams using the com.archimed.dicom.DicomReader class. The DicomReader class has a number of methods for reading DICOM data.

DicomObject read (java.io.InputStream in)

This method can read both raw data sets and DICOM files and will return a DicomObject containing all the data elements within the DICOM file or raw data set. When the supplied inputstream points a to DICOM File, the File Meta Information is also read and stored in a separate DicomObject retrievable with getFileMetaInformation(). When the supplied inputstream points to a raw data set, the transfer syntax of the data set is assumed to be Implicit VR Little Endian. See the other read methods for reading raw data sets in other transfer syntaxes.

The simplest way of using this method is with the following code:

FileInputStream fin = new FileInputStream ();
DicomReader dcmReader = new DicomReader();
DicomObject dcm = dcmReader.read(fin);

DicomObject read (java.io.InputStream in,boolean readpixels)

This method does the same as the previous one but in addition allows you to specify whether to parse or skip pixel data during the reading. This method is mainly here for backwards compatibility. Skipping arbitrary tags can also be achieved by registering a TagReadListener on the DicomReader.

DicomObject read (java.io.InputStream in, int transfersyntax , boolean readpixels)

Use this method to read a raw data set. The transfer syntax is not detected but must be specified. Note: The DicomObject class also contains a number of read methods. These methods are maintained for backwards compatibility. New implementations should use the DicomReader class to read DICOM files and data sets.

2.3. Listening To Tag Read Events

A TagReadListener makes it possible to listen to the reading of individual data elements and possibly modify the DicomObject while it is being read. TagReadListeners are registered with a DicomReader. During the reading of a DICOM file or raw data set, the DicomReader will fire a TagReadEvent just before reading the value field of a data element for which TagReadListeners are registered. The TagReadEvent contains a property dataReadStatus that determines how the value field of a data element is read after all listeners are notified:

READ_TAG_DATA After all the listeners have been notified the DicomReader will continue to read the value field of the data element normally as any other data element. This is the default.

SKIP_TAG_DATA The DicomReader will skip the entire value field of the data element and will place an empty data element in the DicomObject. The underlying inputstream will be moved forward to the beginning of the next data element by the DicomReader.

SKIP_TAG The DicomReader will skip the entire value field of the data element. No empty data element is places within the DicomObject. The underlying inputstream will be moved forward to the beginning of the next data element by the DicomReader.

NEXT_TAG The DicomReader assumes that one of the listener will read the value field of the data element and that when all listeners are notified, the inputstream is positioned just before the next data element or the end of the stream if this was the last data element in the data set.

2.4. Writing DICOM Files and Data Sets

DICOM files and raw datasets (without the 128 byte file header and 0x0002 group) can be written to arbitrary input streams using the com.archimed.dicom.DicomWriter class. The DicomWriter class has three write methods for writing to output streams. These methods allow you to control:

  • File Meta Information

  • Transfer Syntax

  • Generation of group length data elements

  • Writing sequences with defined or undefined length

    DicomWriter.write ( DicomObject dcm, OutputStream out, boolean dicomfile)

This method writes a DicomObject to an outputstream as a DICOM file or a data set depending on the boolean argument dicomfile.

File Meta Information: The File Meta Information to be used when writing a DICOM file, can be specified through the FileMetaInformation property of the DicomObject. If this property contains a non-null DicomObject, the File Meta Information data elements - data elements of group 2 - of the DicomObject contained in this property are used. If this property is null, the DicomReader generates File Meta Information with the following data elements:

Tag

Name

Value

(0002,0001)

File Meta Information Version

0001h

(0002,0002)

Media Storage SOP Class UID

initialized by SOP Class UID or exception thrown if absent

(0002,0003)

Media Storage SOP Instance UID

initialized by SOP Instance UID or exception thrown if absent

(0002,0010)

Transfer Syntax UID

Implicit VR Little Endian

(0002,0012)

Implementation Class UID

initialized by constant JDT.DEFAULT_IMPLEMENTATION_CLASS_UID

(0002,0013)

Implementation Version Name

initialized by constant JDT.DEFAULT_IMPLEMENTATION_VERSION_NAME

Transfer Syntax: When writing a raw Data Set, the transfer syntax is Implicit VR Little Endian. When writing a DICOM file, the Transfer Syntax is determined by the FileMetaInformation. If no File Meta Information is specified, the DICOM file is written in Implicit VR Little Endian Transfer Syntax.

Sequence lengths: sequences are written with undefined length when this write method is used.

Group lengths: group lengths are generated and added to the data set during to writing to the outputstream.

DicomWriter.write ( DicomObject dcm, OutputStream out, boolean dicomfile, int transfersyntax, boolean sequencesUndefined)

This method writes a DicomObject to an outputstream as a DICOM file or a data set depending on the boolean argument dicomfile.

File Meta Information: The File Meta Information to be used when writing a DICOM file, can be specified through the FileMetaInformation property of the DicomObject. If this property contains a non-null DicomObject, the File Meta Information data elements - data elements of group 2 - of the DicomObject contained in this property are used. If this property is null, the DicomReader generates File Meta Information with the following data elements:

Tag

Name

Value

(0002,0001)

File Meta Information Version

0001h

(0002,0002)

Media Storage SOP Class UID

initialized by SOP Class UID or exception thrown if absent

(0002,0003)

Media Storage SOP Instance UID

initialized by SOP Instance UID or exception thrown if absent

(0002,0010)

Transfer Syntax UID

initialized by the Transfer Syntax specified in the transfersyntax argument

(0002,0012)

Implementation Class UID

initialized by constant JDT.DEFAULT_IMPLEMENTATION_CLASS_UID

(0002,0013)

Implementation Version Name

initialized by constant JDT.DEFAULT_IMPLEMENTATION_VERSION_NAME

Transfer Syntax: specified by the transfersyntax argument.

Sequence lengths: sequences are written with undefined length when the value of the sequencesUndefined argument is true and written with defined length when the value is false.

Group lengths: group lengths are generated and added to the data set during to writing to the outputstream.

DicomWriter.write ( DicomObject dcm,OutputStream out, boolean dicomfile,
int transfersyntax, boolean sequencesUndefined, boolean groupLengths)

This method is identical to the previous write method with the exception that the generation of group length data elements can be turned on or off depending on the value of the groupLengths argument.

Note: The DicomObject class also contains a number of write methods. These methods are maintained for backwards compatibility. New implementations should use the DicomWriter class to write DICOM files and data sets.

2.5. Java DICOM Type Map

The DICOM standard defines a number of data types known as value representations (VR) and specifies for every data element which value Representation to use for data typing and formatting the value(s) of that data element. Similarly the Java Language has a number of data types such as String,Integer,Float,…​ and JDT has defined a few extra custom types to represent values of data elements.

JDT contains two types of setters/getters to retrieve and modify values of data elements in a dataset:

  • Typesafe set,add or get methods in the DataElement class and DicomObject class that take or return a specific data type like String,Float,Person,…​

  • Type unsafe set,add or get methods that take or return an instance of java.lang.Object

Deprecation Note: Type unsafe getters and setters like DicomObject.set(java.lang.Object), java.lang.Object DicomObject.get() are deprecated and new implementations should avoid using them.

Type Map

The map below specifies the supported getters and setters of the DicomObject class and DataElement class. For example, the AE value representation has String and byte[] as supported types for typesafe setters and getters of DicomObject and DataElement. This means that for AE data elements the all set,add and get methods are supported that take or return a byte array or a String as an argument or return type. Other set, add or get methods will throw an exception.

All value representations except SQ support getString(), but for some this is only to get a human readable representation of the value of tag, possibly truncated. This is especially the case for data elements with VR of OW,OB,UN.

DICOM VR

Supported typesafe getters

Supported typesafe setters

AE

String, byte[]

String, byte[]

AS

String, byte[]

String, byte[]

AT

ATValue, String

ATValue

CS

String, byte[]

String, byte[]

DA

String, byte[]

String, byte[]

DS

BigDecimal, String, byte[]

BigDecimal, String, byte[]

DT

String, byte[]

String, byte[]

FL

String, Float

String,Float

FD

String, Double

String,Double

IS

Integer, Long, Short, String, byte[]

Integer, Long, Short, String, byte[]

LO

String, byte[]

String, byte[]

LT

String

String

OB

byte[], String

byte[], String

OW

byte[], String

byte[], String

OF

float[], String

float[]

PN

Person, String, byte[]

Person, String, byte[]

SH

String, byte[]

String, byte[]

SL

Long, Integer, String

Long,Integer,Short,String

SQ

getSequenceItem(DicomObject)

addSequenceItem(DicomObject), setSequenceItem(DicomObject)

SS

Long, Integer, Short, String

Long, Integer, Short, String

ST

String, byte[]

String, byte[]

TM

String, byte[]

String, byte[]

UI

String, byte[]

String, byte[]

UL

Long, String

Long, Integer, Short, String

UN

byte[], String

byte[], String

US

Long, Integer, String

Long, Integer, Short, String

UT

String, byte[]

String, byte[]

2.6. Using the UID Registry

The DICOM standard defines a registry of unique identifiers (UID) that are used throughout the standard. JDT has defined registry classes that hold these unique identifiers together with a description. More specifically JDT defines registry classes for

  • SOP Class UIDs: com.archimed.dicom.SOPClass

  • Meta SOP Class UIDs: com.archimed.dicom.MetaSOPClass

  • Transfer Syntax UIDs: com.archimed.dicom.TransferSyntax

  • SOP Instance UIDs: com.archimed.dicom.SOPInstance

All UID registry classes have defined constants which are used to identify the UIDs contained in the registry.

All registry classes inherit from the com.archimed.dicom.UID superclass. Registry entries are represented as com.archimed.dicom.UIDEntry objects.

The class com.archimed.dicom.SOPClass represents every publicly defined SOP class as an int constant and as a constant of type SOPClassUID. For example, the Basic Film Box SOP class is both represented as

public static final int BasicFilmBox;
public static final SOPClassUID BasicFilmBoxUID;

The class com.archimed.dicom.TransferSyntax represents every publicly defined transfer syntax as an int constant and as a constant of type TransferSyntaxUID. For example, the Basic Film Box SOP class is both represented as

public static final int ImplicitVRLittleEndian;
public static final TransferSyntaxUID ImplicitVRLittleEndianUID;

2.6.1. Working with the registry classes

The UID class provides methods for looking up UIDs based on their value or based on a defined constant corresponding to the UID. For example, to retrieve the UIDEntry instance for the JPEG Baseline Transfer Syntax based on the constant defined in the TransferSyntax registry class:

UIDEntry entry = UID.getUIDEntry(TransferSyntax.JPEGBaseline);

or alternatively using the value of UID:

UIDEntry entry = UID.getUIDEntry("1.2.840.10008.1.2.4.50");

A UIDEntry has the following properties:

  • The name of the UID

  • The value of the UID

  • The constant representing the UID in the registry class

  • The type of UID (SOP Class,Transfer Syntax,Meta SOP Class,SOP Instance)

When using int constants of the class SOPClass and TransferSyntax in UID.getUIDEntry, the returned UIEntry objects are in fact the defined SOPClassUID constants and TransferSyntaxUID constants. So the following holds: TransferSyntax.JPEGBaselineUID == UID.getUIDEntry(TransferSyntax.JPEGBaseline)

2.6.2. Adding new entries to the SOP Class registry

New UIDs can be added dynamically to The SOP Class registry with the addEntry methods of SOPClass. The addEntry method returns a constant that can be used to identify the newly added UID based on a constant. A possible way of doing this is by subclassing SOPClass:

public class MySOPClass extends SOPClass
{
  public static int MySOP;

  static
  {
    try
    {
      UIDEntry entry = new UIDEntry(-1, "1.2.3.4.5", "My SOP Storage", "MY", UIDEntry.SOPClass);
      MySOP = SOPClass.addEntry(entry);
      }catch(DicomException ex)
      {
        ex.printStackTrace();
      }
    }
  }
}

2.6.3. Storage SOP Classes

The SOPClass registry class provides methods to list Storage SOP Classes UIDs and to check if a SOP Class UID actually represents a Storage SOP Class:

  • listStorageSOPClasses: provides a List of UIDEntry objects of all Storage SOP Classes in the SOPClass registry

  • isStorageSOPClass: expects a SOP Class constant and returns true or false depending on whether the constant represents a Storage SOP Class or not.

2.7. Using the Data Element Registry

The DICOM standard defines a registry of all public DICOM Data Elements. This registry assigns a unique tag, a name, a value representation and semantics to every Data Element that it contains. As the DICOM standard evolves, this registry grows together with the DICOM data model.

The com.archimed.dicom.DDict class represents the Data Element Registry. It defines a constant for every Data Element defined within the registry and it contains the Value Representation, group-element pair and description of every Data Element. The name of the constant is in most cases the concatenated description for the Data Element given in the DICOM Standard with the exception that illegal characters according to the Java syntax are removed. The DDict class has constants defined for:

  • DICOM Command Elements (group 0x0000)

  • DICOM Data Elements

  • DICOM File Meta Elements (group 0x0002)

  • DICOM Directory Structuring Elements (group 0x0004)

The DDict class also defines constants for all Value Representations.

A number of set and get methods of DicomObject expect a DDict constant as one of it’s arguments to identify the Data Element for which to set or retrieve values.

2.7.1. Retrieving information about a Data Element registered in DDict

The DDict class contains methods to retrieve

  • the description

  • the group-element pair

  • the value representation as a constant

  • the value representation as a 2-character string

for a Data Element registered with DDict and given a DDict constant representing a Data Element.

2.7.2. Adding new tag definitions to DDict

You can add extra entries to DDict using the addEntry method. This method returns a constant that you can use to identify the Data Element in DicomObject methods that expect a DDict constant. A possible way of adding new entries to DDict is by subclassing it in the following way:

 import com.archimed.dicom.*;

 public class MyDDict
 {
  public static int dMyTag1;
  public static int dMyTag2;

   //When this class is used for the first time, the tags are initialized and added to the DDict.
   //The constants used for referencing the tags are also initialized.

  static
  {
     try
     {
       dMyTag1 = DDict.addEntry(new DDictEntry(0x0011,0x0002,DDict.tFD,"My Tag 1","1"));
       dMyTag2 = DDict.addEntry(new DDictEntry(0x0011,0x0007,DDict.tLO,"My Tag 2","1"));
     }
     catch(DicomException e)
     {
       System.out.println("unable to add private tags to dictionary: " + e);
     }
   }
 }

The MyDDict class can be used as follows:

 DicomObject dcm = new DicomObject();
 dcm.set(DDict.dPatientName,"Doe^John");

 //Add some tags of MyDDict
 dcm.set(MyDDict.dMyTag1,new Double(123.456));
 dcm.set(MyDDict.dMyTag2,"My Value";

2.8. Retrieving values of a Data Element

A DICOM file and a Data Set are represented in memory by an object of class com.archimed.dicom.DicomObject. This class acts as a container of Data Elements. Depending on the Value Multiplicity a Data Element can hold one or more values. Data Elements can also be empty , which means that they are present in the Data Set or DICOM file but they don’t contain any values: the length of their value field is 0. The Java type of the retrieved values follows the Java-DICOM type map.

2.8.1. Retrieving the number of values stored in a Data Element

The getSize method and getSize_ge method return the number of values stored a Data Element. If the Data Element is present within the DICOM File or Data Set but is empty, the methods will return 0. If the Data Element is not present, the methods will return -1.

For example, suppose a DicomObject dcm contains an ImageType Data Element with values 'DERIVED\SECONDARY'. (The backslash separates the different values in the Data Element for this particular Value Representation). The number of values in this example is 2:

int multiplicity = dcm.getSize(DDict.dImageType); //will return value 2

or alternatively with (group,element) pair:

int multiplicity = dcm.getSize_ge(0x0008,0x0008); //will return value 2

2.8.2. Retrieving values of a Data Element not contained in a Sequence Item

A] The Data Element contains a single value

Use the get method of DicomObject that takes as an argument either the corresponding DDict constant of the Data Element or use the get_ge method that takes as arguments the group and element number of the Data Element.

For example, to retrieve the value of the Patient ID Data Element using it’s group and element pair (0x0010,0x0020):

String patientID = (String) dcm.get_ge (0x0010,0x0020);

Otherwise the value can also be retrieved using the DDict constant DDict.dPatientID corresponding to the Data Element:

String patientID = (String) dcm.get (DDict.dPatientID);

In this example the value is returned as String, because the patient ID Data Element has Value Representation LO which is mapped to the String class.

B] The Data Element contains multiple values

A Data Element can have a multiplicity greater than 1, which means that there can be multiple values stored within the Data Element. To retrieve a particular value out of a Data Element containing multiple values use the get or get_ge method with an additional index argument.

Use the getSize method to retrieve the actual number of values stored within the Data Element.

For example, suppose a DicomObject dcm contains an ImageType Data Element with multiplicity 2 and values 'DERIVED\SECONDARY'. (The backslash separates the different values in the Data Element for this particular Value Representation). You retrieve the values as follows:

String str1 = (String)dcm.get(DDict.dImageType,0); //will return 'DERIVED' String str2 = (String)dcm.get(DDict.dImageType,1); //will return 'SECONDARY'

Alternatively using the (group,element) pair:

String str1 = (String)dcm.get(0x0008,0x0008,0); //will return 'DERIVED'
String str2 = (String)dcm.get(0x0008,0x0008,1); //will return 'SECONDARY'

2.8.3. Retrieving values of a Data Element contained in a Sequence Item

Values of a Data Element contained within a Sequence are retrieved in much the same way as values with a multiplicity greater than one. When a Data Element in a DicomObject represents a Sequence (DICOM Value Representation SQ), then the individual items of the Sequence are internally stored as objects of type DicomObject themselves and can be retrieved with the same get methods of DicomObject.

The returned object will be itself an instance of the DicomObject class.

For example, suppose a DicomObject dcm contains a Scheduled Procedure Step Sequence with 3 Sequence Items and suppose a value must be retrieved of the Scheduled AE Title Data Element in the second Sequence Item.

int numberofitems = dcm.getSize( DDict.dScheduledProcededureStepSequence );
DicomObject item = (DicomObject)dcm.get( DDict.dScheduledProcededureStepSequence ,1);
String aeTitle = (String)item.get( DDict.dScheduledStationAETitle );

There is no limit on the levels of nesting of sequences. A DicomObject that is retrieved as an item of a Sequence can itself contain Sequences.

A utility class com.archimed.dicom.tools.Sequences exist which contains various methods for direct retrieval of values of Data Elements contained within items of a Sequence. This class supports retrieving values in Data Elements contained in Sequence Items up to two levels deep.

Group lengths are not stored within a DicomObject so there is no way of retrieving group lengths.

2.9. Adding and Updating values of a Data Element

The DicomObject class contains set and and append methods to add and update values of Data Elements. A Data Element can be identified by specifying a DDict constant or by specifying the group-element pair of the Data Element.

2.9.1. Adding or Updating a Data Element with a Single Value

For example use the DDict constant DDict.dPatientID to add a new Patient ID Data Element to a DicomObject or update an existing Patient ID Data Element:

DicomObject dcm = new DicomObject();
dcm.set(DDict.dPatientID,"PTI12324");

or alternatively use the group-element pair of the Patient ID Data Element:

DicomObject dcm = new DicomObject();
dcm.set_ge(0x0010,0x0020,"PTI12324");

If this method is used on a Data Element which contains multiple values, the first value is updated.

2.9.2. Making a Data Element empty

To add an empty Data Element or to update a Data Element so that is becomes empty, use null as an argument:

DicomObject dcm = new DicomObject(); dcm.set(DDict.dPatientID,null); Adding or Updating a Data Element with a Multiple Values

To add a value to a Data Element with multiple values or to update a particular value in a Data Element with multiple values, use the set or set_ge methods that take an extra index argument:

dcm.set(DDict.dImageType,0,"DERIVED");
dcm.set(DDict.dImageType,1,"SECONDARY");

One can also use an append method to add multiple values to a Data Element:

dcm.append(DDict.dImageType,"DERIVED");
dcm.append(DDict.dImageType,"SECONDARY");

In the example above, the first call to append will create a new Data Element in the DicomObject with a single value 'DERIVED' and the second call to append will add an extra value 'SECONDARY' to the Data Element.

The set and set_ge methods will throw a DicomException if the specified index is greater than the current number of values contained in the Data Element. For example the following code will throw a DicomException at the second call to set:

dcm.set(DDict.dImageType,0,"DERIVED");
dcm.set(DDict.dImageType,3,"SECONDARY");

2.9.3. Making a specific value of multi-valued Data Element empty

To set a specific value in a multi-valued Data Element to empty, use the set method with a null argument and with the particular index:

dcm.set(DDict.dOtherPatientID,0,"AKA22334");
dcm.set(DDict.dOtherPatientID,1,null);
dcm.set(DDict.dOtherPatientID,2,"AZE-2322-2099");

In the example above, the second value of the Other Patient ID Data Element will be set to empty.

2.9.4. Removing a Data Element

//Use the deleteItem method to remove an entire Data Element from a DicomObject:
List l = dcm.deleteItem(DDict.dPatientID);

A List with all the values of the removed Data Element is returned.

2.9.5. Removing a particular value from a Data Element

//Use the deleteItem method with an extra index argument to remove a particular value from a Data Element:
Object o = dcm.deleteItem(DDict.dOtherPatientID,2);

The value that is removed from the Data Element is returned. The values with a higher index are shifted downwards.

2.9.6. Sequences

Sequence Items are represented as instances of DicomObject. To manipulate values of Data Elements contained within a Sequence Item, use the set,append and deleteItem methods on the instance of the DicomObject representing the Sequence Item. The add,modify and remove entire Sequence Items in a Sequence, use the set,append and deleteItem methods on the DicomObject that contains the Sequence.

For example, to add a Sequence Data Element to a DicomObject:

//create a first sequence item
DicomObject sequenceItem1 = new DicomObject();
sequenceItem1.set(DDict.dRetrieveAETitle,"SERVER12");
sequenceItem1.set(DDict.dSeriesInstanceUID,"1.295.34533.33335.23");
//create a second sequence item
DicomObject sequenceItem2 = new DicomObject();
sequenceItem2.set(DDict.dRetrieveAETitle,"SERVER07");
sequenceItem2.set(DDict.dSeriesInstanceUID,"1.295.34533.35454.17");
//add the items to a Sequence Data Element of a DicomObject
DicomObject dcm = new DicomObject();
dcm.set(DDict.dReferencedSeriesSequence,1,sequenceItem1);
dcm.set(DDict.dReferencedSeriesSequence,2,sequenceItem2);

2.10. Listing the contents of a DICOM file

On a number of occasions it can be useful to generate a list of the Data Elements of a DICOM file as formatted text. This can be done directly with the DicomObject.dumpVRs methods or with the class com.archimed.dicom.DumpUtils that gives some more control over the formatting. Regardless of which method is used, all data elements are always listed in a tabular format with columns

  • tag number of the data element

  • data element description

  • VR type

  • length

  • multiplicity

  • value

To reflect the nested sequence structure of DICOM file, Data elements contained in sequences are indented. The size of the indentation is dependent on the sequence depth. Values of type OW or OB (eg: pixeldata) are cutoff at a fixed number of bytes.

2.10.1. Using the dump methods in DicomObject

The DicomObject class contains two methods that write the contents of the object to an outputstream as formatted text:

DicomObject.dumpVRs (java.io.OutputStream os) DicomObject.dumpVRs (java.io.OutputStream os,boolean metainfo) The first method will list the data elements of the File Meta Information. The second method will list the data elements of the File Meta Information if the metainfo argument is set to true.

2.10.2. Using the com.archimed.dicom.DumpUtils class

The com.archimed.dicom.DumpUtils class allows one to specify:

  • the character width of the data element description column

  • the character width of the value column

  • the character width of the indention for data elements contained in sequences

  • whether or not to list File Meta Information Data Elements.

2.10.3. Examples

DicomObject dcm = new DicomObject;
dcm.read(new FileInputStream("foo.dcm"));
dcm.dumpVRs(System.out, true);
DicomObject dcm = new DicomObject;
dcm.read(new FileInputStream("foo.dcm"));
DumpUtils du = new DumpUtils(40,70,8,true);
du.dump(dcm,os);

2.11. Working with extended or replacements charsets

The DICOM standard supports the usage of non-ASCII character sets for various value representations (see Dicom Standard Part V) . If a DICOM file or DICOM dataset has tag values encoded in other character sets than the default , the used character sets must be specified in the SpecificCharacterSet tag (0x0008,0x0005). In general this tag can be multi-valued as multiple character sets can be used in a single DICOM file or DICOM dataset with the code extensions technique.

When retrieving non-default charset encoded values out of DicomObjects, JDT will represent them as String objects hereby decoding the values in the dataset or DICOM file into a Unicode String. JDT relies here on the fact that Java Strings are Unicode and every used DICOM character set can be properly represented as a java String object. JDT will use the character sets specified in the SpecificCharacterSet tag when converting data element values into Java Strings.

2.11.1. Retrieving values encoded with a non-default charset

When retrieving values as Strings for value representations that support non-default charsets, JDT will try to convert the values to proper Unicode Strings. For example, suppose we have a DicomObject which contains a value for the StudyDescription tag encoded in a non-default charset. Then doing

String studyDescription = dcm.getString(DDict.dStudyDescription,0); will retrieve the first value of the StudyDescription tag into a String. JDT will internally take into account the value of the SpecificCharset tag to do a correct conversion into a Unicode String.

2.11.2. Setting values encoded in a non-default charset and using multiple character sets

When using the DicomObject.setString methods on data elements with a value representation that supports non-default charsets, JDT will try to encode the String according to the character sets specified in the SpecificCharacterSet data element of the DicomObject:

  • If the SpecificCharacterSet data element is empty or missing, JDT will try to encode the string using the DICOM default character set (US-ASCII)

  • If the SpecificCharacterSet data element contains a single value, JDT will try to encode the entire string with the specified character set stored in the single value

  • If the SpecificCharacterSet data element is multi-valued, JDT will try to encode every character of the specified string with the first character set in the SpecificCharacterSet data element.If a character cannot be encoded using the first character set of the SpecificCharacterSet data element, JDT will try with the other character sets in following the order of the values of the SpecificCharacterSet element, until one is found that can encode the character or an Exception is thrown if no character set is able to encode the character.

When encoding with multiple character sets (.3), JDT will use proper character set escape sequences according to the code extension technique described in the DICOM standardand will take into account DICOM control characters that may be present in the strings to be encoded and that trigger switching of the active character set to the first character set of a multi-valued SpecificCharacterSet data element.

For example suppose we have a java String object studyDescription that contains characters in Greek. This value can be encoded properly in the Greek characterset (ISO_IR_126) if before the studyDescription value is set, the SpecificCharset tag is initialized:

DicomObject dcm = new DicomObject();
dcm.setString(DDict.dSpecificCharacterSet,DicomCharset.ISO_IR_126.getDefinedTerm(),0);
dcm.setString(DDict.dStudyDescription,0,studyDescription, DicomCharset.ISO_IR_126);
In order for this to work with DicomObjects that are sequence items, the items must first be set into their parent DicomObject.

For example:

 DicomObject parent = new DicomObject();
 DicomObject item = new DicomObject();

//always set the charset tag first
parent.setString(DDict.dSpecificCharacterSet,DicomCharset.ISO_IR_126.getDefinedTerm(),0);

//link the item with its parent first before encoding Strings in non-default charsets
parent.setSequenceItem(DDict.dSomeSequence,item,0);

//now the string with non-default characters can be set properly
item.setString(DDict.dSomeTag,0,someStringWithGreekChars, DicomCharset.ISO_IR_126);

This will allow the setString method call to lookup the specified character set in the parent DicomObject.

2.11.3. Using Unicode UTF-8 character sets (ISO_IR 192)

DICOM supports the use of Unicode encoded in UTF-8, which makes it possible to support different languages all with the same encoding. The use of of Unicode UTF-8 is recommended when it is a requirement to support multiple character sets covering different languages.

When creating new DICOM files or datasets that will contain tags encoded in Unicode UTF-8, one must set the SpecificCharacterSet tag to DicomCharset.ISO_IR_192.

DicomObject dcm = new DicomObject();
dcm.setString(DDict.dSpecificCharacterSet, DicomCharset.ISO_IR_192.getDefinedTerm(), 0);

2.11.4. Retrieving PN values encoded with non-default charsets

One of the typical uses of non-default charsets is in the encoding of PN data elements.To retrieve the value of a PN data element you can do

Person patientName = dcm.getPersonName(DDict.dPatientName, 0);

The different attributes of the Person object, each representing a person name component (eg lastname,firstname,…​) of a particular component group(single byte,phonetic,ideographic) will be properly filled in if available in the tag value. JDT will convert the patient name into a Unicode String representation and fill all components for the 3 component groups. Alternatively one can also do

String patientName = dcm.getString(DDict.dPatientName, 0);

This will result in a person name string according to the DICOM format for PN valued tags with group delimiters(^)and component delimiters(=).

Lastly every data element value can always be retrieved directly as byte[] array.

byte[] patientName = dcm.getBytes(DDict.dPatientName, 0);
In order for java Strings containing non-ASCII characters to be displayed properly in a GUI or on the command-line, the chosen fonts must support the characters represented in the java Strings

2.11.5. Using ideographic and phonetic representations in PN tags (code extension techniques)

JDT supports the use of code extension techniques in PN data elements. This allows you to encode the ideographic and phonetic representations of a person name in other character sets. This is typically useful in Japanese and Korean. The DicomObject class a version of setPersonName and addPersonName with which you can specify the character sets that should be used for the encoding of the three different component groups (single-byte,ideographic and phonetic) of a PN data element.

In order to produce correct DICOM files and datasets, the used character sets must also be specified in the SpecificCharset (0008,0005) tag. In particular the used charset for the single-byte component group of a PN data element must correspond with the first value of a multi-valued SpecificCharset tag. An empty first value of the SpecificCharset tag means that the default charset (ISO_IR_6) must be used for the single-byte component group.

2.11.6. Example: Using ideographic and phonetic representations in Japanese (Yamada Tarou)

Appendix H of Part 5 of the DICOM standard contains examples of PN data elements with ideographic and phonetic representations in Japanese character sets. Here we show how to reproduce one of these examples.

If you start from an new DicomObject, first create and set the SpecificCharset tag:

//create a new DicomObject and initialize SpecificCharset tag
//with charset that you want to use in the DicomObject
DicomObject dcm = new DicomObject();
dcm.setString(DDict.dSpecificCharacterSet,DicomCharset.ISO_2022_IR_13.getDefinedTerm(),0);
dcm.setString(DDict.dSpecificCharacterSet,DicomCharset.ISO_2022_IR_87.getDefinedTerm(),1);

Create a new Person object and fill it in properly. For the sake of the example, we initialize the values of the ideographic and phonetic representations through Unicode escape sequences. In a normal application the String properties for the ideographic and phonetic representations will most often be initialized from keyboard input.

Person person = new Person();
person.familyName            = "Yamada";
person.givenName             = "Tarou";
person.familyNameIdeographic = "\u5c71\u7530";
person.givenNameIdeographic  = "\u592a\u90ce";
person.familyNamePhonetic    = "\u3084\u307e\u3060";
person.givenNamePhonetic     = "\u305f\u308d\u3046";

Use the setPersonName method of DicomObject that takes the used charsets as extra arguments. There is one charset argument per component group of a PN tag.

//create an empty patient name tag and fill it with the values of the Yamada Tarou file
dcm.setPersonName(DDict.dPatientName, person, 0, DicomCharset.ISO_2022_IR_6,
DicomCharset.ISO_2022_IR_87,
DicomCharset.ISO_2022_IR_87);

3. Basic DICOM Networking

This chapter discusses basic DICOM networking with classes found in the com.archimed.dicom.network package. For several services classes more higher-end classes are available in the com.archimed.dicom.scu package that implement complete conversations including association establishment, exchange of the necessary DIMSE messages and association teardown.

3.1. Requesting an Association

Before DIMSE messages can be exchanged between AEs, an Association between the AEs has to be established. When your application is the initiator of DIMSE messages you need to create a TCP/IP network connection with the AE with which you want to exchange messages and request an Association over the TCP/IP network connection. The remote AE will either acknowledge or reject the Association request. Typically SCUs are the initiator of DIMSE messages and thus request the creation of an Association with remote AEs. For example a Storage SCU that wants to send a DICOM image to a Storage SCP , will typically create a TCP/IP connection to the Storage SCP and request an Association on the TCP/IP connection. Also SCPs sometimes need to request Associations when they need to send N-EVENT-REPORT DIMSE messages.

3.1.1. Step 1: Create a TCP/IP connection

The first step in requesting an Association is the creation of a TCP/IP connection with a remote AE. For example:

Socket s = new Socket("pacs1.acme.com",104);

This creates a open socket connection on port 104 to the server with DNS name pacs1.acme.com.

3.1.2. Step 2: Create an AssociationIO Object

You need a com.archimed.dicom.network.AssociationIO object to exchange DIMSE messages. The AssociationIO object is initialized with the inputstream and outputstream of the open TCP/IP connection.

InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
AssociationIO as = new AssociationIO(in,out);

The AssociationIO object will be used to send the Association request, receive the response and to send and receive all DIMSE messages.

3.1.3. Step 3: Create an Association Request

The Association Request contains important information such as our AE title, the remote AE title, the SOP Classes for which we want to setup an association and the transfer syntaxes that we want to use. For example:

 Request request = new Request();
 request.setCalledTitle("IMG_ARCHIVE1");
 request.setCallingTitle("IMG_WORKSTATION1");

 int[] transfersyntaxes = {TransferSyntax.ImplicitVRLittleEndian};
 request.addPresentationContext(1,SOPClass.SecondaryCaptureImageStorage,transfersyntaxes);

The proposed presentation context is here created with presentation context ID 1. This number is used later on in identifying in which presentation context DIMSE message are exchanged. One can propose multiple presentation contexts in one Request. The presentation context ID must be odd, between 1 and 255 and unique per proposed presentation context. In the Request one can optionally specify:

  • The Implementation Class UID and Implementation Version Name (otherwise defaults apply)

  • the SCU/SCP role selection for every abstract syntax

  • the maximum number of operations invoked and performed

  • the maximum PDU size that we are willing to receive

  • extended negotiation data

3.1.4. Step 4: Send the Association Request

When the Request is created and properly configured, it can be sent to the remote AE with the AssociationIO object:

as.write(request);

The write method will format the request as an A-ASSOCIATE-RQ PDU and send it to the remote AE.

3.1.5. Step 5: Read the response from the remote AE

When the remote AE has received the Association Request, it will process it and send a response. In general this can be an Association Acknowledge or an Association Reject. If an unexpected condition occurs the AE might also send an Associate Abort.

For example to receive and interpret the response from the remote AE:

//read the response from the network
ULServiceMessage ulServiceMessage = as.read();

//process the response
switch (ulServiceMessage.getMessageType())
{
  case ULServiceMessage.ASS_ACKNOWLEDGE:
    System.out.println("received associate aknowledge");
    break; //jump out of the switch statement
  case ULServiceMessage.ASS_REJECT:
    System.out.println("Association rejected by remote AE");
    //dump the content of the Reject to System.out
    System.out.println(ulServiceMessage);
    //close the TCP/IP connection
    s.close();
    return;
  case ULServiceMessage.ABORT:
    System.out.println("Association aborted by remote AE");
    //dump the content of the Abort to System.out
    System.out.println(ulServiceMessage);
    //close the TCP/IP connection
    s.close();
    return;
  default:
    System.out.println("unexpected message received from remote AE");
    //dump the content
    System.out.println(ulServiceMessage);
    //close the TCP/IP connection
    s.close();
    return;
  }
//here starts the exchange of DIMSE messages
//...

The code above is merely an example. Real applications would handle the different message types more thoroughly. See the com.archimed.dicom.examples.Example1 class for a complete example of the setup of an association.

3.2. Accepting an Association

When a DICOM Application Entity is the Acceptor of Associations it must be able to listen to incoming Associate Request messages and respond with Acknowledge or Reject messages. Such application entities typically wait for incoming TCP/IP connection requests on a certain port number. Once a TCP/IP connection is established the application reads the incoming Associate Request message, processes the message and responds with an Associate Acknowledge or Associate Reject message. Application Entities that act as the SCP for Service Classes must be able to accept incoming association requests. Application Entities that act as an SCU for Service Classes that provide notifications through the N-EVENT-REPORT DIMSE message, must also be able to accept incoming Association Requests.

The functionality of listening to TCP/IP connection requests and establishing TCP/IP connections is not embedded within the JDT classes. Use the classes of the java.net package or an alternative TCP/IP network package to listen to and establish connections. JDT requires an inputstream and outputstream of an open connection for reading and writing data.

Typically applications that accept Association requests and provide the exchange of DIMSE messages over established associations, act as a server and must be able to handle a number of active associations concurrently. JDT makes no assumptions on the choice of server architecture and how the concurrent connections are managed.

3.2.1. Step 1: Create an AssociationIO object

We start from the point where a TCP/IP connection has been established and a valid socket is available. Similar as when sending an Associate Request, we need a com.archimed.dicom.network.AssociationIO object to exchange messages.

Socket s = ...;
AssociationIO as = new AssociationIO(s.getInputStream(),s.getOutputStream());

3.2.2. Step 2: Read the Associate Request from the remote AE

The first message sent on the TCP/IP connection will be the Association Request from the requestor to the acceptor. With the AssociationIO class we read the message from the connection and check if it is actually an Associate Request:

ULServiceMessage ulServiceMessage = as.read();

if (ulServiceMessage.getMessageType() != ULServiceMessage.ASS_REQUEST)
{
  System.out.println("unexpected message received from remote AE");
  System.out.println(ulServiceMessage);
  return;
}
System.out.println("incoming associate request");
System.out.println(ulServiceMessage);

3.2.3. Step 3: Create an Associate Acknowledge or Associate Reject

The incoming Associate Request has to be interpreted and a proper response must be sent back to the remote AE. The content of the Request object can be inspected with the various methods of the Request class. Alternatively one can make use of the com.archimed.dicom.network.ResponsePolicy class to create a Reject or Acknowledge based on the contents of the incoming Request and a number of criteria. Consult the API of the ResponsePolicy class for details. For example:

int[] allowedSOPClasses = {SOPClass.Verification};
ULServiceMessage resMessage = ResponsePolicy.prepareResponse((Request)ulServiceMessage,
 "IMG_ARCHIVE1",null,allowedSOPClasses,TransferSyntax.ImplicitVRLittleEndian,true);

The prepareResponse method of the ResponsePolicy class creates a Reject object or an Acknowledge object based on whether the Request is acceptable or not given the different criteria in the arguments of the method. In this particular example the method will return an Acknowledge object if the called AE title in the Request equals 'IMG_ARCHIVE1' and if there is a presentation context proposed for the Verification SOP Class and the Implicit VR Little Endian Transfer Syntax. Because the last argument is set to true, method will return a Reject message in all other cases.

The use of the ResponsePolicy class is optional. Implementations that need more refined control over the construction of an Acknowledge and Reject can create and configure these objects directly without the use of the ResponsePolicy class.

3.2.4. Step 4: Send the response back to the remote AE

The response message must be sent back to the remote AE. This is again done with the AssociationIO instance. Depending on whether the response is a Reject or Acknowledge the program flow can be different.

//write back the response
as.write(resMessage);
if (resMessage.getMessageType() == ULServiceMessage.ASS_REJECT)
{
  System.out.println("association was rejected");
  System.out.println(resMessage);
  return;
}

//continue here with exchanging DIMSE messages
//...

After an Acknowledge message is sent the association is established and the exchange of DIMSE message can begin.

See the com.archimed.dicom.examples.Example2 class for a complete example of the setup of an association as an association acceptor.

3.3. Exchanging DIMSE messages

After an association has been established between two AEs, the AEs can begin exchanging DIMSE messages. These messages are sent and received with the same AssociationIO instance used to establish the association.

DIMSE messages always have a Command part and can optionally have a Data Set carrying the data associated with the Command. For example the C-ECHO-RQ message , which is used in the Verification SOP Class , has no associated Data Set. The C-STORE-RQ message, used in the Storage Service Class, always has a related Data Set.

When two AEs are connected through an association, one AE will always take the role of invoking DIMSE-Service-User and will send Command Request Messages. The other AE will take the role of performing DIMSE-Service-User and will answer the Command Request messages with Command Response messages. Except for the C-CANCEL Command Request messages (C-CANCEL-FIND,C-CANCEL-MOVE and C-CANCEL-GET) every Command Request message has a corresponding Command Response message. For example the C-STORE-RQ message has a corresponding C-STORE-RES message.

A Command Request message can be answered with multiple Command Response messages. For example a C-FIND-RQ message used to query a PACS system with the Query/Retrieve Service class, can result in multiple C-FIND-RES messages being returned.

Every DIMSE message is sent in the context of a negotiated presentation context. The presentation context ID is used to identify in which presentation context a DIMSE message is sent.

The creation and handling of Command Request messages and Command Response messages is done with the com.archimed.dicom.network.Command class and the com.archimed.dicom.network.Dimse class. The Dimse class represents a complete Command Request or Command Response message including the used presentation context ID, the Command part of the message and an optional Data Set. The Command class represents the Command part of the DIMSE message. Because a Command is a special kind of Data Set, the Command class is a subclass of com.archimed.dicom.DicomObject.

See the topics on

  • Sending a DIMSE Command Request

  • Receiving a DIMSE Command Response

  • Receiving a DIMSE Command Request

  • Sending a DIMSE Command Response

for details on sending and receiving DIMSE messages as an invoking DIMSE-Service-User or a performing DIMSE-Service-User.

3.4. Sending a DIMSE Command Request

The invoking DIMSE-Service-User sends Command Request messages to a remote AE and receives Command Response messages from the remote AE. For DIMSE messages related to an operation service such as the C-STORE service or the N-CREATE service the invoking DIMSE-Service-User has the role of the Service Class User (SCU). For DIMSE messages related to a notification service - the N-EVENT-REPORT message - the invoking DIMSE-Service-User has the role of a Service Class Provider (SCP).

You use the static create methods of the Command class to create new Command instances for request messages and you use the Dimse class to create the actual messages containing the presentation context ID, the Command object and an optional Data Set. To send the messages, you must use the AssociationIO instance that was used for establishing the Association. For example to send a C-ECHO-RQ message to a remote AE, which never has an associated data set, you would typically do something like:

AssociationIO as;
//assume a valid AssociationIO instance
//create a C-ECHO-RQ Command for the Verification SOP Class with message ID 1
Command cmd = Command.createCEchoReq(1,SOPClass.Verification);

//create a DIMSE message around the previously created Command without a Data Set
//and in presentation context 1. Here is assumed that the Verification SOP Class
//was actually proposed and accepted in a presentation context with ID equal to 1.
Dimse cEchoReq = new Dimse(1,cmd,null);

//send the DIMSE message
as.write(cEchoReq);

3.4.1. Sending an Associated Data Set

In the event that a data set is associated with the command, the code remains the same except that the data set in the form of a DicomObject is specified in the Dimse constructor. For example in a C-STORE-RQ message for an image:

AssociationIO as;
DicomObject dcm;

//assume a valid AssociationIO instance and a DicomObject
//containing a composite SOP Instance of an image
//create a C-STORE-RQ Command for the SecondaryCaptureImageStorage SOP Class
//with message ID 1, priority 0 and SOP Instance UID 1.2.3.4
Command cmd = Command.createCStoreReq(1,SOPClass.SecondaryCaptureImageStorage,0,"1.2.3.4");

//create a DIMSE message around the previously created Command and the data set
//for presentation context 2. Here is assumed that the SecondaryCaptureImageStorage SOP Class
//was actually proposed and accepted in a presentation context with ID equal to 2.
Dimse cStoreReq = new Dimse(2,cmd,dcm);

//send the DIMSE message
as.write(cStoreReq);

See Example1.java for a complete example of sending a C-ECHO-RQ message and receiving a C-ECHO-RES message.

3.5. Receiving a DIMSE Command Response

You must use the same AssociationIO instance for reading a Command Response message from the network than the instance used for establishing the Association and sending the Command Request. The read method of AssociationIO will return a Dimse object that contains the presentation context ID of the presentation context used for sending the Command Response, the Command itself and an optional Data Set.

//read response
ULServiceMessage ulServiceMessage = as.read();

//check that the response is actually a DIMSE message
if (ulServiceMessage.getMessageType() \!= ULServiceMessage.DIMSE)
{
  System.out.println("received unexpected message,sending abort");
  as.write(new Abort(Abort.DICOM_UL_SERVICE_PROVIDER,Abort.UNEXPECTED_PDU);
  s.close();
  return;
}

//dump the response on screen as a Data Set
System.out.println("received the following response:");
cmd = ((Dimse)ulServiceMessage).getCommand();
cmd.dumpVRs(System.out);
System.out.println();

//check the status in the C-ECHO-RES
if ((cmd.getCommandType() == Command.C_ECHO_RESPONSE) && (cmd.getStatus() == 0x0000))
{
  System.out.println("remote AE indicates verification OK");
}

3.5.1. Reading an Associated Data Set

Command Response messages can contain a Data Set. The Data Set can be retrieved as an InputStream with the getDataSetInputStream method of the Dimse class or directly as a DicomObject with the getDataSet method of the Dimse class. The two methods should not be used together on the same Dimse instance.

The inputstream returned by getDataSetInputStream contains the data set unwrapped from the PDU structures. The stream can be used to save the data set as it is received from the remote AE or it can for example be used with a DicomReader to read the Data Set into a DicomObject.

The first call to the getDataSet method will read the entire Data Set into a DicomObject and return it. A reference to this DicomObject instance is held by the Dimse instance and is returned in subsequent calls to getDataSet.

3.6. Receiving a DIMSE Command Request

The performing DIMSE-Service-User receives Command Requests after an association has been established. The same AssociationIO instance that was used to establish an association, must be used to read DIMSE Command Requests and write DIMSE Command Responses.

The following example shows the reception of a C-ECHO-RQ on an established association.

//read the DIMSE message, should be a C-ECHO-RQ
ulServiceMessage = as.read();

//check that the message is in fact a DIMSE message
if (ulServiceMessage.getMessageType() != ULServiceMessage.DIMSE)
{
  System.out.println("unexpected message received, aborting");
  as.write(new Abort(Abort.DICOM_UL_SERVICE_USER,Abort.UNEXPECTED_PDU));
  return;
}

//we know here that the ULServiceMessage is a of class Dimse so we can cast it safely
Dimse dimse = (Dimse)ulServiceMessage;

//dump the contents of the command to System.out as a DicomObject
Command cmd = dimse.getCommand();
System.out.println(">>received following command request");
cmd.dumpVRs(System.out);

//check that the Command is a C-ECHO-RQ
if (cmd.getCommandType() != Command.C_ECHO_REQUEST)
{
  System.out.println("unexpected command request, aborting");
  as.write(new Abort(Abort.DICOM_UL_SERVICE_USER,Abort.UNEXPECTED_PDU_PARAMETER));
  return;
}

//further processing of the C-ECHO-RQ
//...

See Example2.java for a complete example of receiving a C-ECHO-RQ message and sending back a C-ECHO-RES message.

In general an association may be established with a number of accepted presentation contexts corresponding to different service classes. As a result a performing DIMSE-Service-User may not know in advance which type of Command Request it is going to receive and it should be prepared to receive multiple types of Command Requests and process them accordingly. The program structure must reflect this so that the program flow is determined by the type of Command Request that is received.

3.6.1. Reading an Associated Data Set

Command Request messages can optionally have a related Data Set. The reading of such a Data Set is similar to when reading a Data Set in a Command Response.

3.7. Sending a DIMSE Command Response

After the performing DIMSE-Service-User has received and interpreted a Command Request message, he answers with one or more Command Response messages. The Command class contains a number of static methods that create proper Command Responses for corresponding Command Requests. The methods will create Command Responses with a message ID corresponding to the Command Request specified in the first argument. The other arguments in the create methods are used the specify values of additional Data Elements present in the Command Response.

For example, to create a C-ECHO-RES response message for a C-ECHO-RQ request message:

//assume a Command from a C-ECHO-RQ message
Command cmd;
//...

//create a C-ECHO-RES command with corresponding message ID and status 0 (means OK)
Command resCmd = Command.createCEchoRes(cmd,0);

//create the DIMSE message, null indicates no associated Data Set
Dimse cEchoRes = new Dimse(dimse.getPresentationContextId(),resCmd ,null);

//send the Command Response
as.write(cEchoRes);

3.7.1. Sending an Associated Data Set in a Command Response

A DIMSE Command Response can optionally have an associated Data Set that is sent after the Command part. For example, in response to a C-FIND-RQ message a performing DIMSE-Service-User must send a C-FIND-RES message including a Data Set for every SOP instance that matches the query criteria included in the identifier (data set) of the C-FIND-RQ message and finally an extra C-FIND-RES message indicating the end of the query results without an associated Data Set. The sending of an associated Data Set in a Command Response is identical to sending an associated Data Set in a Command Request.

3.8. Releasing an Association

After the exchange of DIMSE messages is completed, the requestor of an Association must release the Association. The requestor of the Association must send a Release Request message and the acceptor of the Association must respond with a Release Response message:

//release the association
as.write(new ReleaseRequest());
System.out.println("sent release request");

//read the response
ulServiceMessage = as.read();
if (ulServiceMessage.getMessageType == ULServiceMessage.REL_REQUEST)
{
  System.out.println("received release response");
}
else
{
  System.out.println("unexpected message received from remote AE");
  System.out.println(ulServiceMessage);
}

3.9. Aborting an Association

Under normal circumstances an Abort message is never sent to a remote AE. However, in the event that an exception occurs and the association with the remote AE must be terminated, you can send an Abort message with a source and reason parameter. For example:

AssociationIO as;
Socket s;

//assume a properly initialized Socket and AssociationIO instance
Abort abort = new Abort(Abort.DICOM_UL_SERVICE_PROVIDER,Abort.REASON_NOT_SPECIFIED);
as.write(abort);
s.close();

3.10. Configuring Transport Layer Security (TLS)

Configuring TLS works in general by replacing the default socket factory with a com.archimed.dicom.network.tls.ConfigureTLSSocketFactory instance. Use the static factory method ConfigureTLSSocketFactory.getFactory to create a socket factory capable of creating TLS sockets with various cipher suites, TLS protocols, keystores and trust stores. For example:

SocketFactory socketFactory = ConfigurableTLSSocketFactory.getFactory(clientCertFile,
    clientCertPassword, trustStoreFile, trustStorePassword, tlsProtocols, cipherSuites);
Socket socket = socketFactory.createSocket("pacs01.acme.com",3006);
AssociationIO associationIO =
    new AssociationIO(socket.getInputStream(),socket.getOutputStream());

Sockets created from this socket factory will encrypt according to the specified protocols and cipher suites. The enum class ConfigurableTLSSocketFactory.ConnectionProfile contains predefined TLS protocols and cipher suites according to the connection profiles BCP195 and BCP195 non-downgrading specified in the DICOM standard (PS 3.15)

4. SCU Network classes

The com.archimed.dicom.scu package contains a number of classes that implement a complete SCU (Service Class User). The package contains factory classes for performing network operations in one call or creating SCU instances that can be used to perform multiple consecutive network operations on an existing association.

4.1. General usage pattern

All implementations of com.archimed.dicom.scu follow a common usage pattern.

// create a an SCU instance from a factory class
MultiCStoreSCU multiCStoreSCU = CStoreSCUFactory.createMultiCStoreSCU(....

//optionally configure the SCU instance further by using common setters
multiStoreSCU.setReadTimeout(5000);
...

// create an association request.
// The returned assocationRequest instance is the one that will be proposed to the remote device
AssociationRequest associationRequest = multiCStoreSCU.createAssociationRequest();

// perform the setup of a network connection and association negotation with the remote device
// the associationResult can be inspected to learn what was accepted by the remoted device
AssociationResult associationResult = multiCStoreSCU.associate();

// execute one or more operations
MultiCStoreResult multiCStoreResult = multiCStoreSCU.executeStore(null, Command.MEDIUM_PRIORITY);

// after all operations are performed, release the association
multiCStoreSCU.release();

The factory classes also contain a few methods that execute a complete DICOM network conversation including association establishment, performing an operation like a verification (DICOM ping) or store operation and then release the association.

4.2. Configuring TLS and common network parameters

All SCU instances are subclasses of the interface com.archimed.dicom.scu.SCU and share functionality to configure TLS and a number of common network parameters like timeouts:

scu.setSocketFactory(ConfigurableTLSSocketFactory.getFactory(...);
scu.setConnectTimeout(5000);  //milliseconds
scu.setReadTimeout(5000);     //milliseconds
scu.setMaxSendPduSize(16384);    //bytes
scu.setMaxReceivePduSize(16384); //bytes
scu.setUserIdentityNegotationRequest(...);

Refer to Configuring Transport Layer Security for more information.

4.3. Verification (C-ECHO)

Use the com.archimed.dicom.scu.CEchoSCUFactory class to perform a DICOM ping (C-ECHO) to a remote device in one go:

CEchoSCUFactory.executeCEcho("pacs01.acme.com",104,"REMOTEAE","LOCALAE");

When the ping is succesful, the call will simply return. When the ping fails, the call will throw an Exception.

If the network connection should be secure and other parameters like timeouts must be configured, do something like

try {
  CEchoSCU cEchoSCU = CEchoSCUFactory.createCEchoSCU("pacs01.acme.com",104,"REMOTEAE","LOCALAE");
  scu.setSocketFactory(ConfigurableTLSSocketFactory.getFactory(...);
  scu.setConnectTimeout(5000);  //milliseconds
  scu.setReadTimeout(5000);     //milliseconds
  scu.setMaxSendPduSize(16384);    //bytes
  scu.setMaxReceivePduSize(16384); //bytes
  scu.setUserIdentityNegotationRequest(...);
  int code = cEchoSCU.execute();
  StatusCode statusCode = StatusCode.forCEcho(code);
  //inspect the statusCode to determine wether or not the C-ECHO was succesful
} catch (IOException ex) {
  //handle IOExceptions ,possibily related to timeouts
}

4.4. Store (C-STORE)

Create a MultiCStoreSCU instance by using one of the factory methods of com.archimed.dicom.scu.CStoreSCUFactory. The various factory methods differ only in how the DICOM SOP instances like images to be stored are specified:

  • com.archimed.dicom.DicomObject instances including valid file meta information

  • java.io.File objects pointing to Part10 DICOM Files

  • java.util.Iterator of java.io.InputStream instances where each inputstream holds a Part10 DICOM file

When the DICOM SOP instances are specified as DicomObject instances or File objects, JDT will read them in fully in memory before sending the SOP instances down the network via a C-STORE (unless the DicomObject is constructed with BulkDataReference data elements).

When the DICOM SOP instances are specified as an Iterator of inputstreams, the SOP instances contained in the inputstreams are directly streamed onto the network encapsulated in a C-STORE request, hereby conserving memory usage. In this case all the combinations of SOP classes and transfer syntaxes that will occur in the iterator, must be specified upfront in the factory method, so that the proper association with support for all these combinations can be proposed.

4.5. Query (C-FIND)

Documentation pending…​

4.6. Retrieve (C-GET)

A C-GET is used to retrieve DICOM SOP instances like images from a remote DICOM device. Unlike a C-MOVE, the SOP instances are sent by the remote DICOM device via C-STORE requests over the same network connection and association that is used to for the C-GET request and response.

Use the CGetSCUFactory to create a MultiCGetSCU instance that can perform multiple consecutive C-GET request/responses and receive SOP instances via C-STORE request/responses.

When creating a MultiCGetSCU, the caller must specify the storage SOP classes and transfer syntaxes that need to be negotiated and supported in the association. SOP instances like images that match the specified identifier but have a SOP class or transfer syntax not negotiated , cannot be retrieved.

MultiCGetSCU multiCGetSCU = CGetSCUFactory.createMultiCGetSCU("pacs01.acme.com",
    104,
    "REMOTEAE",
    "LOCALAE",
    SOPClass.StudyRootQueryRetrieveInformationModelGETUID,
    storageSOPClasses,
    transferSyntaxUIDS,
    cGetExtendedNegotiation); //optional extended negotiation

//configure TLS and timeouts (see Verification example above)
//multiCGetSCU.setConnectTimeout(5000);
//...

//create an 'identifier' which contains the query filters for the SOP instances to be retrieved
DicomObject identifier = new DicomObject();
identifier.setString(DDict.dQueryRetrieveLevel, "STUDY", 0);
identifier.addString(DDict.dStudyInstanceUID, "1.2.3.4");

try {
  //create an association request. This request will be sent to the remote DICOM device with the associate() call
  AssociationRequest associationRequest = multiCGetSCU.createAssociationRequest();
  //send the associate request and return the response from the peer DICOM device
  AssociationResult associationResult = multiCGetSCU.associate();
  //perform the C-GET. Specify a MultiCGetSCUHandler that is capable of handling the incoming SOP instances.
  multiCGetSCU.executeGet(identifier, Command.MEDIUM_PRIORITY, new MyMultiCGetSCUHandler());
  multiCGetSCU.release();
} catch (RejectException ex) {
  Logger.info("<-- association rejected");
} catch (AbortException ex) {
  Logger.info("<-- association aborted");
} catch (Exception ex){
  //handle exceptions
}

4.7. Retrieve (C-MOVE)

Create a MultiCMoveSCU instance by using one of the factory methods of com.archimed.dicom.scu.CMoveSCUFactory.

A MultiCMoveSCU is capable of sending C-MOVE requests and receiving C-MOVE responses. A MultiCMoveInstance has various executeCMove methods where one can optionally specify an implementation of com.archimed.dicom.scu.MultiCMoveSCUHandler to be notified of C-MOVE responses as they are received from the remote DICOM device.

A MultiCMoveSCU does not by itself run a Storage SCP and is as such not capable of receiving C-STORE requests. Use the com.archimed.dicom.network package classes to implement a Storage SCP that can be used in conjunction with the MultiCMoveSCU and receive SOP instances that are sent by the remote DICOM device as a result of the C-MOVE requests sent by the MultiCMoveSCU to the remote DICOM device.

Alternatively one can you use a MultiCGetSCU to retrieve SOP instances such as images with one network connection and association , initiated by the SCU.

5. Logging

5.1. Introduction

Logging and interpreting log messages can be an invaluable tool during the debugging and testing of your applications and in general provides detailed information on the execution of code. JDT provides a small logging package com.archimed.log that is used internally by the other JDT packages to output log messages during the execution of various methods of JDT classes.

Because JDT is normally integrated into other applications, application developers may have already chosen an existing logging framework like for example Apache Log4J or the java.util.logging package. The com.archimed.log package does not make use directly of any existing logging framework but can be set up so that it delegates the generation of log statements to an underlying existing logging framework. In the absence of an existing logging framework, the package can generate log statements on its own.

5.2. Logging with JDT’s internal logging framework

By default, JDT is configured to use one instance of com.archimed.log.DefaultJdtLogger for writing all log messages from all the JDT classes. The DefaultJdtLogger class defines seven log levels comparable with the Log4J log levels. The default level is OFF which means no log messages are written. The DefaultJdtLogger class holds a reference to a single OutputStream to which all log messages are written and which is initialized to System.out.

For example, to set the output level to the ERROR level and to direct the output of log messages to System.err :

DefaultJdtLogger.setLevel(DefaultJdtLogger.ERROR);
DefaultJdtLogger.setOutputStream(System.err);

5.3. Integrating with an existing logging framework

The logging mechanism in JDT is based on two interfaces:

com.archimed.log.JdtLoggerFactory
com.archimed.log.JdtLogger

The JdtLoggerFactory interface defines one method that returns references to JdtLogger implementing classes. The JdtLogger interface defines the necessary methods for logging messages at various log levels. A JdtLoggerFactory can be specified with the static setJdtLoggerFactory method of the com.archimed.dicom.Jdt class.

All JDT classes that use logging retrieve a reference to a JdtLogger by first retrieving the current JdtLoggerFactory and then retrieving a JdtLogger by using the getJdtLogger class of the factory.

For example the DicomObject class has declared:

protected JdtLogger log = Jdt.getJdtLoggerFactory().getJdtLogger(DicomObject.class);

By default, the installed factory is the com.archimed.log.DefaultJdtLoggerFactory which keeps a reference to a single com.archimed.log.DefaultJdtLogger instance. This factory can be replaced by another factory that returns different implementations of JdtLogger.

In general to replace the default logging with logging from an existing log framework, one has to:

  • create a new factory that implements JdtLoggerFactory and that returns instances of JdtLogger implementing classes

  • create a new JdtLogger implementation, making use of the logging functionality of the existing log framework

5.4. Example: Using Apache Log4j

As an example of replacing the default logging, two classes are provided that enable JDT to use Log4j as the logging framework:

com.archimed.dicom.examples.Log4jLoggerFactory
com.archimed.dicom.examples.Log4jLogger

The two classes wrap functionality of the org.apache.log4j.Logger class. To enable JDT logging using these classes the default factory must be replaced with this new factory:

Jdt.setJdtLoggerFactory(new Log4jLoggerFactory());

From that point on , JDT will log messages according to the current Log4j configuration. For example, to enable DEBUG level logging on all classes of JDT one would typically do:

org.apache.log4j.Logger.getLogger("com.archimed").setLevel(org.apache.log4j.Level.DEBUG);

6. Command Line Utilities

The JDT distribution includes a few command-line utilities in the executable jar jdt-cli.jar. This jar is a fat jar and wraps the core JDT library (jdt.jar). The command-line utilities will search for a valid jdt.key in the directory of the jdt-cli.jar. To invoke a command-line utility , execute something like

java -jar jdt-cli.jar <subcommand>

For general help, execute

java -jar jdt-cli.jar --help

The following subcommands are currently supported:

help

Displays help information about the specified command

dump

Dumps the contents of a DICOM file/stream to stdout

tls

Lists information about TLS protocols and ciphers

echo

Verifies the connection to an SCP with a C-ECHO request

view

Displays a simple Swing viewer pane

search

Search local file systems for DICOM files

store

Storage SCU (C-STORE)

get

Retrieve SCU via C-GET

version

Dumps the version of JDT to stdout

dict

Query the tag,SOP class and transfer syntax dictionaries

For specific help about a subcommand, execute

java -jar jdt-cli.jar help <subcommand>