dicom 0.6.1 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- # Copyright 2008-2009 Christoffer Lervag
1
+ # Copyright 2008-2010 Christoffer Lervag
2
2
 
3
3
  # Some notes about this DICOM file reading class:
4
4
  # In addition to reading files that are compliant to DICOM 3 Part 10, the philosophy of this library
@@ -11,20 +11,19 @@ module DICOM
11
11
  # Class for reading the data from a DICOM file:
12
12
  class DRead
13
13
 
14
- attr_reader :success, :names, :tags, :types, :lengths, :values, :raw, :levels, :explicit, :file_endian, :msg
14
+ attr_reader :success, :names, :tags, :vr, :lengths, :values, :bin, :levels, :explicit, :file_endian, :msg
15
15
 
16
16
  # Initialize the DRead instance.
17
17
  def initialize(string=nil, options={})
18
18
  # Process option values, setting defaults for the ones that are not specified:
19
- @lib = options[:lib] || DLibrary.new
20
19
  @sys_endian = options[:sys_endian] || false
21
- @bin = options[:bin]
20
+ @bin_string = options[:bin]
22
21
  @transfer_syntax = options[:syntax]
23
22
  # Initiate the variables that are used during file reading:
24
23
  init_variables
25
24
 
26
25
  # Are we going to read from a file, or read from a binary string:
27
- if @bin
26
+ if @bin_string
28
27
  # Read from the provided binary string:
29
28
  @str = string
30
29
  else
@@ -43,7 +42,7 @@ module DICOM
43
42
  # Create a Stream instance to handle the decoding of content from this binary string:
44
43
  @stream = Stream.new(@str, @file_endian, @explicit)
45
44
  # Do not check for header information when supplied a (network) binary string:
46
- unless @bin
45
+ unless @bin_string
47
46
  # Read and verify the DICOM header:
48
47
  header = check_header
49
48
  # If the file didnt have the expected header, we will attempt to read
@@ -66,11 +65,11 @@ module DICOM
66
65
  # Post processing:
67
66
  # Assume file has been read successfully:
68
67
  @success = true
69
- # Check if the last element was read out correctly (that the length of its data (@raw.last.length)
68
+ # Check if the last element was read out correctly (that the length of its data (@bin.last.length)
70
69
  # corresponds to that expected by the length specified in the DICOM file (@lengths.last)).
71
70
  # We only run this test if the last element has a positive expectation value, obviously.
72
71
  if @lengths.last.to_i > 0
73
- if @raw.last.length != @lengths.last
72
+ if @bin.last.length != @lengths.last
74
73
  @msg << "Error! The data content read from file does not match the length specified for the tag #{@tags.last}. It seems this is either an invalid or corrupt DICOM file. Returning."
75
74
  @success = false
76
75
  return
@@ -176,17 +175,17 @@ module DICOM
176
175
  return false
177
176
  end
178
177
  # STEP 2: ------------------------------------------------------
179
- # Access library to retrieve the data element name and type (VR) from the tag we just read:
180
- lib_data = @lib.get_name_vr(tag)
178
+ # Access library to retrieve the data element name and VR from the tag we just read:
179
+ lib_data = LIBRARY.get_name_vr(tag)
181
180
  name = lib_data[0]
182
181
  vr = lib_data[1]
183
182
  # (Note: VR will be overwritten if the DICOM file contains VR)
184
183
 
185
184
  # STEP 3: ----------------------------------------------------
186
- # Read type (VR) (if it exists) and the length value:
187
- tag_info = read_type_length(vr,tag)
188
- type = tag_info[0]
189
- level_type = type
185
+ # Read VR (if it exists) and the length value:
186
+ tag_info = read_vr_length(vr,tag)
187
+ vr = tag_info[0]
188
+ level_vr = vr
190
189
  length = tag_info[1]
191
190
 
192
191
  # STEP 4: ----------------------------------------
@@ -195,37 +194,37 @@ module DICOM
195
194
  if @enc_image and tag == "FFFE,E000"
196
195
  # The first item appearing after the image element is a 'normal' item, the rest hold image data.
197
196
  # Note that the first item will contain data if there are multiple images, and so must be read.
198
- type = "OW" # how about alternatives like OB?
197
+ vr = "OW" # how about alternatives like OB?
199
198
  # Modify name of item if this is an item that holds pixel data:
200
199
  if @tags.last != "7FE0,0010"
201
200
  name = "Pixel Data Item"
202
201
  end
203
202
  end
204
203
  # Read the value of the element (if it contains data, and it is not a sequence or ordinary item):
205
- if length.to_i > 0 and type != "SQ" and type != "()"
204
+ if length.to_i > 0 and vr != "SQ" and vr != "()"
206
205
  # Read the element's value (data):
207
- data = read_value(type,length)
206
+ data = read_value(vr,length)
208
207
  value = data[0]
209
- raw = data[1]
208
+ bin = data[1]
210
209
  else
211
210
  # Data element has no value (data).
212
211
  # Special case: Check if pixel data element is sequenced:
213
212
  if tag == "7FE0,0010"
214
- # Change name and type of pixel data element if it does not contain data itself:
213
+ # Change name and vr of pixel data element if it does not contain data itself:
215
214
  name = "Encapsulated Pixel Data"
216
- level_type = "SQ"
215
+ level_vr = "SQ"
217
216
  @enc_image = true
218
217
  end
219
218
  end # of if length.to_i > 0
220
219
  # Set the hiearchy level of this data element:
221
- set_level(level_type, length, tag, name)
220
+ set_level(level_vr, length, tag, name)
222
221
  # Transfer the gathered data to arrays and return true:
223
222
  @names << name
224
223
  @tags << tag
225
- @types << type
224
+ @vr << vr
226
225
  @lengths << length
227
226
  @values << value
228
- @raw << raw
227
+ @bin << bin
229
228
  return true
230
229
  end # of process_data_element
231
230
 
@@ -252,8 +251,8 @@ module DICOM
252
251
  end
253
252
 
254
253
 
255
- # Reads and returns data element TYPE (VR) (2 bytes) and data element LENGTH (Varying length; 2-6 bytes).
256
- def read_type_length(type,tag)
254
+ # Reads and returns data element VR (2 bytes) and data element LENGTH (Varying length; 2-6 bytes).
255
+ def read_vr_length(vr,tag)
257
256
  # Structure will differ, dependent on whether we have explicit or implicit encoding:
258
257
  pre_skip = 0
259
258
  bytes = 0
@@ -261,14 +260,14 @@ module DICOM
261
260
  if @explicit == true
262
261
  # Step 1: Read VR (if it exists)
263
262
  unless tag == "FFFE,E000" or tag == "FFFE,E00D" or tag == "FFFE,E0DD"
264
- # Read the element's type (2 bytes - since we are not dealing with an item related element):
265
- type = @stream.decode(2, "STR")
263
+ # Read the element's vr (2 bytes - since we are not dealing with an item related element):
264
+ vr = @stream.decode(2, "STR")
266
265
  @integrated_lengths[@integrated_lengths.length-1] += 2
267
266
  end
268
267
  # Step 2: Read length
269
- # Three possible structures for value length here, dependent on element type:
270
- case type
271
- when "OB","OW","SQ","UN"
268
+ # Three possible structures for value length here, dependent on element vr:
269
+ case vr
270
+ when "OB","OW","SQ","UN","UT"
272
271
  # 6 bytes total:
273
272
  # Two empty bytes first:
274
273
  pre_skip = 2
@@ -276,11 +275,11 @@ module DICOM
276
275
  bytes = 4
277
276
  when "()"
278
277
  # 4 bytes:
279
- # For elements "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
278
+ # For elements "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD":
280
279
  bytes = 4
281
280
  else
282
281
  # 2 bytes:
283
- # For all the other element types, value length is 2 bytes:
282
+ # For all the other element vr, value length is 2 bytes:
284
283
  bytes = 2
285
284
  end
286
285
  else
@@ -306,23 +305,23 @@ module DICOM
306
305
  # If it is not, it may indicate a file that is not standards compliant or it might even not be a DICOM file.
307
306
  @msg += ["Warning: Odd number of bytes in data element's length occured. This is a violation of the DICOM standard, but program will attempt to read the rest of the file anyway."]
308
307
  end
309
- return [type, length]
310
- end # of read_type_length
308
+ return [vr, length]
309
+ end # of read_vr_length
311
310
 
312
311
 
313
312
  # Reads and returns data element VALUE (Of varying length - which is determined at an earlier stage).
314
- def read_value(type, length)
313
+ def read_value(vr, length)
315
314
  # Extract the binary data:
316
315
  bin = @stream.extract(length)
317
316
  @integrated_lengths[@integrated_lengths.size-1] += length
318
317
  # Decode data?
319
318
  # Some data elements (like those containing image data, compressed data or unknown data),
320
319
  # will not be decoded here.
321
- unless type == "OW" or type == "OB" or type == "OF" or type == "UN"
320
+ unless vr == "OW" or vr == "OB" or vr == "OF" or vr == "UN"
322
321
  # "Rewind" and extract the value from this binary data:
323
322
  @stream.skip(-length)
324
323
  # Decode data:
325
- value = @stream.decode(length, type)
324
+ value = @stream.decode(length, vr)
326
325
  if not value.is_a?(Array)
327
326
  data = value
328
327
  else
@@ -341,7 +340,7 @@ module DICOM
341
340
 
342
341
  # Sets the level of the current element in the hiearchy.
343
342
  # The default (top) level is zero.
344
- def set_level(type, length, tag, name)
343
+ def set_level(vr, length, tag, name)
345
344
  # Set the level of this element:
346
345
  @levels += [@current_level]
347
346
  # Determine if there is a level change for the following element:
@@ -350,7 +349,7 @@ module DICOM
350
349
  # Note the following exception:
351
350
  # If data element is an "Item", and it contains data (image fragment) directly, which is to say,
352
351
  # not in its sub-elements, we should not increase the level. (This is fixed in the process_data_element method.)
353
- if type == "SQ"
352
+ if vr == "SQ"
354
353
  increase = true
355
354
  elsif name == "Item"
356
355
  increase = true
@@ -363,7 +362,7 @@ module DICOM
363
362
  if length.to_i != 0
364
363
  @hierarchy << [length, @integrated_lengths.last]
365
364
  else
366
- @hierarchy << type
365
+ @hierarchy << vr
367
366
  end
368
367
  end
369
368
  # Need to check whether a previous sequence or item has ended, if so the level must be decreased by one:
@@ -448,7 +447,7 @@ module DICOM
448
447
  end
449
448
  end
450
449
  # Query the library with our particular transfer syntax string:
451
- result = @lib.process_transfer_syntax(@transfer_syntax)
450
+ result = LIBRARY.process_transfer_syntax(@transfer_syntax)
452
451
  # Result is a 3-element array: [Validity of ts, explicitness, endianness]
453
452
  unless result[0]
454
453
  @msg+=["Warning: Invalid/unknown transfer syntax! Will try reading the file, but errors may occur."]
@@ -487,10 +486,10 @@ module DICOM
487
486
  # Arrays that will hold information from the elements of the DICOM file:
488
487
  @names = Array.new
489
488
  @tags = Array.new
490
- @types = Array.new
489
+ @vr = Array.new
491
490
  @lengths = Array.new
492
491
  @values = Array.new
493
- @raw = Array.new
492
+ @bin = Array.new
494
493
  @levels = Array.new
495
494
  # Array that will holde any messages generated while reading the DICOM file:
496
495
  @msg = Array.new
@@ -1,4 +1,4 @@
1
- # Copyright 2009 Christoffer Lervag
1
+ # Copyright 2009-2010 Christoffer Lervag
2
2
 
3
3
  module DICOM
4
4
 
@@ -6,16 +6,25 @@ module DICOM
6
6
  # which will act as a simple storage node (a server that receives images).
7
7
  class DServer
8
8
 
9
- attr_accessor :host_ae, :max_package_size, :port, :timeout, :verbose
9
+
10
+ # Run the server and take a block for initializing.
11
+ def self.run(port=104, path='./received/', &block)
12
+ server = DServer.new(port)
13
+ server.instance_eval(&block)
14
+ server.start_scp(path)
15
+ end
16
+
17
+ # Accessible attributes:
18
+ attr_accessor :host_ae, :max_package_size, :port, :timeout, :verbose, :file_handler
10
19
  attr_reader :errors, :notices
11
20
 
12
21
  # Initialize the instance with a host adress and a port number.
13
- def initialize(port, options={})
22
+ def initialize(port=104, options={})
14
23
  require 'socket'
15
24
  # Required parameters:
16
25
  @port = port
17
26
  # Optional parameters (and default values):
18
- @lib = options[:lib] || DLibrary.new
27
+ @file_handler = options[:file_handler] || FileHandler
19
28
  @host_ae = options[:host_ae] || "RUBY_DICOM"
20
29
  @max_package_size = options[:max_package_size] || 32768 # 16384
21
30
  @timeout = options[:timeout] || 10 # seconds
@@ -70,10 +79,9 @@ module DICOM
70
79
  @valid_abstract_syntaxes = Array.new
71
80
  end
72
81
 
73
-
74
- # Start a Storage Content Provider (SCP).
82
+ # Start a Service Class Provider (SCP).
75
83
  # This service will receive and store DICOM files in a specified folder.
76
- def start_scp(path)
84
+ def start_scp(path='./received/')
77
85
  add_notice("Starting SCP server...")
78
86
  add_notice("*********************************")
79
87
  # Initiate server:
@@ -82,16 +90,17 @@ module DICOM
82
90
  loop do
83
91
  Thread.start(@scp.accept) do |session|
84
92
  # Initialize the network package handler for this session:
85
- link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :verbose => @verbose)
93
+ link = Link.new(:host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout, :verbose => @verbose, :file_handler => @file_handler)
86
94
  add_notice("Connection established (name: #{session.peeraddr[2]}, ip: #{session.peeraddr[3]})")
87
95
  # Receive an incoming message:
88
- segments = link.receive_single_transmission(session)
96
+ #segments = link.receive_single_transmission(session)
97
+ segments = link.receive_multiple_transmissions(session)
89
98
  info = segments.first
90
99
  # Interpret the received message:
91
100
  if info[:valid]
92
101
  association_error = check_association_request(info)
93
102
  unless association_error
94
- syntax_result = check_syntax_requests(info)
103
+ syntax_result = check_syntax_requests(link, info)
95
104
  link.handle_association_accept(session, info, syntax_result)
96
105
  if syntax_result == "00" # Normal (no error)
97
106
  add_notice("An incoming association request and its abstract syntax has been accepted.")
@@ -100,10 +109,15 @@ module DICOM
100
109
  link.handle_release(session)
101
110
  else
102
111
  # Process the incoming data:
103
- file_path = link.handle_incoming_data(session, path)
104
- add_notice("DICOM file saved to: " + file_path)
105
- # Send a receipt for received data:
106
- link.handle_response(session)
112
+ success, message = link.handle_incoming_data(session, path)
113
+ if success
114
+ add_notice(message)
115
+ # Send a receipt for received data:
116
+ link.handle_response(session)
117
+ else
118
+ # Something has gone wrong:
119
+ add_error(message)
120
+ end
107
121
  # Release the connection:
108
122
  link.handle_release(session)
109
123
  end
@@ -171,17 +185,17 @@ module DICOM
171
185
 
172
186
  # Check if the requested abstract syntax & transfer syntax are supported:
173
187
  # Error codes are given in the official dicom document, part 08_08, page 39
174
- def check_syntax_requests(info)
188
+ def check_syntax_requests(link, info)
175
189
  result = "00" # (no error)
176
190
  # We will accept any transfer syntax (as long as it is recognized in the library):
177
191
  # (Weakness: Only checking the first occuring transfer syntax for now)
178
- transfer_syntax = info[:ts].first[:transfer_syntax]
179
- unless @lib.check_ts_validity(transfer_syntax)
192
+ transfer_syntax = link.extract_transfer_syntax(info)
193
+ unless LIBRARY.check_ts_validity(transfer_syntax)
180
194
  result = "04" # transfer syntax not supported
181
195
  add_error("Warning: Unsupported transfer syntax received in incoming association request. (#{transfer_syntax})")
182
196
  end
183
197
  # Check that abstract syntax is among the ones that have been set as valid for this server instance:
184
- abstract_syntax = info[:abstract_syntax]
198
+ abstract_syntax = link.extract_abstract_syntax(info)
185
199
  unless @valid_abstract_syntaxes.include?(abstract_syntax)
186
200
  result = "03" # abstract syntax not supported
187
201
  end
@@ -286,5 +300,5 @@ module DICOM
286
300
  end
287
301
 
288
302
 
289
- end
290
- end
303
+ end # of class
304
+ end # of module
@@ -1,4 +1,4 @@
1
- # Copyright 2008-2009 Christoffer Lervag
1
+ # Copyright 2008-2010 Christoffer Lervag
2
2
 
3
3
  # Some notes about this DICOM file writing class:
4
4
  # In its current state, this class will always try to write the file such that it is compliant to the
@@ -14,22 +14,21 @@ module DICOM
14
14
  # Class for writing the data from a DObject to a valid DICOM file.
15
15
  class DWrite
16
16
 
17
- attr_writer :tags, :types, :lengths, :raw, :rest_endian, :rest_explicit
17
+ attr_writer :tags, :vr, :lengths, :bin, :rest_endian, :rest_explicit
18
18
  attr_reader :success, :msg
19
19
 
20
20
  # Initialize the DWrite instance.
21
21
  def initialize(file_name=nil, options={})
22
22
  # Process option values, setting defaults for the ones that are not specified:
23
- @lib = options[:lib] || DLibrary.new
24
23
  @sys_endian = options[:sys_endian] || false
25
24
  @file_name = file_name
26
25
  @transfer_syntax = options[:transfer_syntax] || "1.2.840.10008.1.2" # Implicit, little endian
27
26
 
28
27
  # Create arrays used for storing data element information:
29
28
  @tags = Array.new
30
- @types = Array.new
29
+ @vr = Array.new
31
30
  @lengths = Array.new
32
- @raw = Array.new
31
+ @bin = Array.new
33
32
  # Array for storing error/warning messages:
34
33
  @msg = Array.new
35
34
  # Default values that may be overwritten by the user:
@@ -99,12 +98,12 @@ module DICOM
99
98
  if value_length > size
100
99
  # Start writing content from this data element,
101
100
  # then continue writing its content in the next segments.
102
- # Write tag & type/length:
101
+ # Write tag & vr/length:
103
102
  write_tag(i)
104
- write_type_length(i)
103
+ write_vr_length(i)
105
104
  # Find out how much of this element's value we can write, then add it:
106
105
  available = size - @stream.length
107
- value_first_part = @raw[i].slice(0, available)
106
+ value_first_part = @bin[i].slice(0, available)
108
107
  @stream.add_last(value_first_part)
109
108
  # Add segment and reset:
110
109
  segments << @stream.string
@@ -114,7 +113,7 @@ module DICOM
114
113
  index = available
115
114
  # Iterate through the data element's value until we have added it entirely:
116
115
  remaining_segments.times do
117
- value = @raw[i].slice(index, size)
116
+ value = @bin[i].slice(index, size)
118
117
  index = index + size
119
118
  @stream.add_last(value)
120
119
  # Add segment and reset:
@@ -183,14 +182,14 @@ module DICOM
183
182
  tag = @stream.encode_tag("0002,0012")
184
183
  @stream.add_last(tag)
185
184
  @stream.encode_last("UI", "STR")
186
- value = @stream.encode_value(@implementation_uid, "STR")
185
+ value = @stream.encode_value(UID, "STR")
187
186
  @stream.encode_last(value.length, "US")
188
187
  @stream.add_last(value)
189
188
  # Implementation Version Name:
190
189
  tag = @stream.encode_tag("0002,0013")
191
190
  @stream.add_last(tag)
192
191
  @stream.encode_last("SH", "STR")
193
- value = @stream.encode_value(@implementation_name, "STR")
192
+ value = @stream.encode_value(NAME, "STR")
194
193
  @stream.encode_last(value.length, "US")
195
194
  @stream.add_last(value)
196
195
  # Group length:
@@ -201,9 +200,9 @@ module DICOM
201
200
  # Length:
202
201
  length = @stream.encode(4, "US")
203
202
  @stream.add_first(length)
204
- # Type:
205
- type = @stream.encode("UL", "STR")
206
- @stream.add_first(type)
203
+ # VR:
204
+ vr = @stream.encode("UL", "STR")
205
+ @stream.add_first(vr)
207
206
  # Tag:
208
207
  tag = @stream.encode_tag("0002,0000")
209
208
  @stream.add_first(tag)
@@ -216,8 +215,8 @@ module DICOM
216
215
  def write_data_element(i)
217
216
  # Step 1: Write tag:
218
217
  write_tag(i)
219
- # Step 2: Write [type] and value length:
220
- write_type_length(i)
218
+ # Step 2: Write [vr] and value length:
219
+ write_vr_length(i)
221
220
  # Step 3: Write value:
222
221
  write_value(i)
223
222
  # If DICOM object contains encapsulated pixel data, we need some special handling for its items:
@@ -242,9 +241,9 @@ module DICOM
242
241
  end
243
242
 
244
243
 
245
- # Writes the type (VR) (if it is to be written) and length value
244
+ # Writes the VR (if it is to be written) and length value
246
245
  # (these two are the middle part of the data element):
247
- def write_type_length(i)
246
+ def write_vr_length(i)
248
247
  # First some preprocessing:
249
248
  # Set length value:
250
249
  if @lengths[i] == nil
@@ -265,14 +264,14 @@ module DICOM
265
264
  if @explicit == true
266
265
  # Step 1: Write VR (if it is to be written)
267
266
  unless @tags[i] == "FFFE,E000" or @tags[i] == "FFFE,E00D" or @tags[i] == "FFFE,E0DD"
268
- # Write data element type (VR) (2 bytes - since we are not dealing with an item related element):
269
- vr = @stream.encode(@types[i], "STR")
267
+ # Write data element VR (2 bytes - since we are not dealing with an item related element):
268
+ vr = @stream.encode(@vr[i], "STR")
270
269
  add(vr)
271
270
  end
272
271
  # Step 2: Write length
273
- # Three possible structures for value length here, dependent on data element type:
274
- case @types[i]
275
- when "OB","OW","SQ","UN"
272
+ # Three possible structures for value length here, dependent on data element vr:
273
+ case @vr[i]
274
+ when "OB","OW","SQ","UN","UT"
276
275
  if @enc_image
277
276
  # Item under an encapsulated Pixel Data (7FE0,0010):
278
277
  # 4 bytes:
@@ -291,22 +290,22 @@ module DICOM
291
290
  add(length4)
292
291
  else
293
292
  # 2 bytes:
294
- # For all the other data element types, value length is 2 bytes:
293
+ # For all the other data element vr, value length is 2 bytes:
295
294
  add(length2)
296
- end # of case type
295
+ end
297
296
  else
298
297
  # *****IMPLICIT*****:
299
298
  # No VR written.
300
299
  # Writing value length (4 bytes):
301
300
  add(length4)
302
301
  end
303
- end # of write_type_length
302
+ end # of write_vr_length
304
303
 
305
304
 
306
305
  # Writes the value (last part of the data element):
307
306
  def write_value(i)
308
307
  # This is pretty straightforward, just dump the binary data to the file/string:
309
- add(@raw[i])
308
+ add(@bin[i])
310
309
  end
311
310
 
312
311
 
@@ -347,7 +346,7 @@ module DICOM
347
346
  # Changes encoding variables as the file writing proceeds past the initial 0002 group of the DICOM file.
348
347
  def switch_syntax
349
348
  # The information from the Transfer syntax element (if present), needs to be processed:
350
- result = @lib.process_transfer_syntax(@transfer_syntax.rstrip)
349
+ result = LIBRARY.process_transfer_syntax(@transfer_syntax.rstrip)
351
350
  # Result is a 3-element array: [Validity of ts, explicitness, endianness]
352
351
  unless result[0]
353
352
  @msg << "Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."
@@ -405,9 +404,6 @@ module DICOM
405
404
  end
406
405
  # Items contained under the Pixel Data element needs some special attention to write correctly:
407
406
  @enc_image = false
408
- # Version information:
409
- @implementation_uid = "1.2.826.0.1.3680043.8.641"
410
- @implementation_name = "RUBY_DICOM_0.6"
411
407
  end
412
408
 
413
409
  end # of class