dicom 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,11 +3,8 @@ module DICOM
3
3
 
4
4
  # This class contains code for handling the client side of DICOM TCP/IP network communication.
5
5
  #
6
- # === Resources
7
- #
8
- # * For information regarding query, such as required/optional attributes and value matching, refer to the DICOM standard, PS3.4, C 2.2.
9
- #--
10
- # FIXME: The code which waits for incoming network packets seems to be very CPU intensive. Perhaps there is a more elegant way to wait for incoming messages?
6
+ # For more information regarding queries, such as required/optional attributes
7
+ # and value matching, refer to the DICOM standard, PS3.4, C 2.2.
11
8
  #
12
9
  class DClient
13
10
  include Logging
@@ -31,26 +28,17 @@ module DICOM
31
28
 
32
29
  # Creates a DClient instance.
33
30
  #
34
- # === Notes
35
- #
36
- # * To customize logging behaviour, refer to the Logging module documentation.
37
- #
38
- # === Parameters
39
- #
40
- # * <tt>host_ip</tt> -- String. The IP adress of the server which you are going to communicate with.
41
- # * <tt>port</tt> -- Fixnum. The network port to be used.
42
- # * <tt>options</tt> -- A hash of parameters.
43
- #
44
- # === Options
31
+ # @note To customize logging behaviour, refer to the Logging module documentation.
45
32
  #
46
- # * <tt>:ae</tt> -- String. The name of this client (application entity).
47
- # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
48
- # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
49
- # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
33
+ # @param [String] host_ip the IP adress of the server which you are going to communicate with
34
+ # @param [Integer] port the network port to be used
35
+ # @param [Hash] options the options to use for the network communication
36
+ # @option options [String] :ae the name of this client (application entity)
37
+ # @option options [String] :host_ae the name of the server (application entity)
38
+ # @option options [Integer] :max_package_size the maximum allowed size of network packages (in bytes)
39
+ # @option options [Integer] :timeout the number of seconds the client will wait on an answer from a server before aborting (defaults to 10)
50
40
  #
51
- # === Examples
52
- #
53
- # # Create a client instance using default settings:
41
+ # @example Create a client instance using default settings
54
42
  # node = DICOM::DClient.new("10.1.25.200", 104)
55
43
  #
56
44
  def initialize(host_ip, port, options={})
@@ -78,7 +66,7 @@ module DICOM
78
66
  @link = Link.new(:ae => @ae, :host_ae => @host_ae, :max_package_size => @max_package_size, :timeout => @timeout)
79
67
  end
80
68
 
81
- # Tests the connection to the server by performing a C-ECHO.
69
+ # Tests the connection to the server by performing a C-ECHO procedure.
82
70
  #
83
71
  def echo
84
72
  # Verification SOP Class:
@@ -88,24 +76,22 @@ module DICOM
88
76
 
89
77
  # Queries a service class provider for images (composite object instances) that match the specified criteria.
90
78
  #
91
- # === Parameters
92
- #
93
- # * <tt>query_params</tt> -- A hash of query parameters.
79
+ # === Instance level attributes for this query:
94
80
  #
95
- # === Options
81
+ # * '0008,0018' (SOP Instance UID)
82
+ # * '0020,0013' (Instance Number)
96
83
  #
97
- # * <tt>"0008,0018"</tt> -- SOP Instance UID
98
- # * <tt>"0020,0013"</tt> -- Instance Number
84
+ # In addition to the above listed attributes, a number of "optional" attributes
85
+ # may be specified. For a general list of optional object instance level attributes,
86
+ # please refer to the DICOM standard, PS3.4 C.6.1.1.5, Table C.6-4.
99
87
  #
100
- # === Notes
88
+ # @note Caution: Calling this method without parameters will instruct your PACS
89
+ # to return info on ALL images in the database!
90
+ # @param [Hash] query_params the query parameters to use
91
+ # @option query_params [String] 'GGGG,EEEE' a tag and value pair to be used in the query
101
92
  #
102
- # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL images in the database!
103
- # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
104
- # * For a general list of optional object instance level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.5, Table C.6-4.
105
- #
106
- # === Examples
107
- #
108
- # node.find_images("0020,000D" => "1.2.840.1145.342", "0020,000E" => "1.3.6.1.4.1.2452.6.687844")
93
+ # @example Find all images belonging to a given study and series
94
+ # node.find_images('0020,000D' => '1.2.840.1145.342', '0020,000E' => '1.3.6.1.4.1.2452.6.687844')
109
95
  #
110
96
  def find_images(query_params={})
111
97
  # Study Root Query/Retrieve Information Model - FIND:
@@ -128,27 +114,25 @@ module DICOM
128
114
 
129
115
  # Queries a service class provider for patients that match the specified criteria.
130
116
  #
131
- # === Parameters
117
+ # === Instance level attributes for this query:
132
118
  #
133
- # * <tt>query_params</tt> -- A hash of parameters.
119
+ # * '0008,0052' (Query/Retrieve Level)
120
+ # * '0010,0010' (Patient's Name)
121
+ # * '0010,0020' (Patient ID)
122
+ # * '0010,0030' (Patient's Birth Date)
123
+ # * '0010,0040' (Patient's Sex)
134
124
  #
135
- # === Options
125
+ # In addition to the above listed attributes, a number of "optional" attributes
126
+ # may be specified. For a general list of optional object instance level attributes,
127
+ # please refer to the DICOM standard, PS3.4 C.6.1.1.2, Table C.6-1.
136
128
  #
137
- # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
138
- # * <tt>"0010,0010"</tt> -- Patient's Name
139
- # * <tt>"0010,0020"</tt> -- Patient ID
140
- # * <tt>"0010,0030"</tt> -- Patient's Birth Date
141
- # * <tt>"0010,0040"</tt> -- Patient's Sex
129
+ # @note Caution: Calling this method without parameters will instruct your PACS
130
+ # to return info on ALL patients in the database!
131
+ # @param [Hash] query_params the query parameters to use
132
+ # @option query_params [String] 'GGGG,EEEE' a tag and value pair to be used in the query
142
133
  #
143
- # === Notes
144
- #
145
- # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL patients in the database!
146
- # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
147
- # * For a general list of optional patient level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.2, Table C.6-1.
148
- #
149
- # === Examples
150
- #
151
- # node.find_patients("0010,0010" => "James*")
134
+ # @example Find all patients matching the given name
135
+ # node.find_patients('0010,0010' => 'James*')
152
136
  #
153
137
  def find_patients(query_params={})
154
138
  # Patient Root Query/Retrieve Information Model - FIND:
@@ -172,25 +156,23 @@ module DICOM
172
156
 
173
157
  # Queries a service class provider for series that match the specified criteria.
174
158
  #
175
- # === Parameters
176
- #
177
- # * <tt>query_params</tt> -- A hash of query parameters.
159
+ # === Instance level attributes for this query:
178
160
  #
179
- # === Options
161
+ # * '0008,0060' (Modality)
162
+ # * '0020,000E' (Series Instance UID)
163
+ # * '0020,0011' (Series Number)
180
164
  #
181
- # * <tt>"0008,0060"</tt> -- Modality
182
- # * <tt>"0020,000E"</tt> -- Series Instance UID
183
- # * <tt>"0020,0011"</tt> -- Series Number
165
+ # In addition to the above listed attributes, a number of "optional" attributes
166
+ # may be specified. For a general list of optional object instance level attributes,
167
+ # please refer to the DICOM standard, PS3.4 C.6.1.1.4, Table C.6-3.
184
168
  #
185
- # === Notes
169
+ # @note Caution: Calling this method without parameters will instruct your PACS
170
+ # to return info on ALL series in the database!
171
+ # @param [Hash] query_params the query parameters to use
172
+ # @option query_params [String] 'GGGG,EEEE' a tag and value pair to be used in the query
186
173
  #
187
- # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL series in the database!
188
- # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
189
- # * For a general list of optional series level attributes, refer to the DICOM standard, PS3.4 C.6.1.1.4, Table C.6-3.
190
- #
191
- # === Examples
192
- #
193
- # node.find_series("0020,000D" => "1.2.840.1145.342")
174
+ # @example Find all series belonging to the given study
175
+ # node.find_series('0020,000D' => '1.2.840.1145.342')
194
176
  #
195
177
  def find_series(query_params={})
196
178
  # Study Root Query/Retrieve Information Model - FIND:
@@ -215,29 +197,27 @@ module DICOM
215
197
 
216
198
  # Queries a service class provider for studies that match the specified criteria.
217
199
  #
218
- # === Parameters
219
- #
220
- # * <tt>query_params</tt> -- A hash of query parameters.
221
- #
222
- # === Options
223
- #
224
- # * <tt>"0008,0020"</tt> -- Study Date
225
- # * <tt>"0008,0030"</tt> -- Study Time
226
- # * <tt>"0008,0050"</tt> -- Accession Number
227
- # * <tt>"0010,0010"</tt> -- Patient's Name
228
- # * <tt>"0010,0020"</tt> -- Patient ID
229
- # * <tt>"0020,000D"</tt> -- Study Instance UID
230
- # * <tt>"0020,0010"</tt> -- Study ID
200
+ # === Instance level attributes for this query:
231
201
  #
232
- # === Notes
202
+ # * '0008,0020' (Study Date)
203
+ # * '0008,0030' (Study Time)
204
+ # * '0008,0050' (Accession Number)
205
+ # * '0010,0010' (Patient's Name)
206
+ # * '0010,0020' (Patient ID)
207
+ # * '0020,000D' (Study Instance UID)
208
+ # * '0020,0010' (Study ID)
233
209
  #
234
- # * Caution: Calling this method without parameters will instruct your PACS to return info on ALL studies in the database!
235
- # * In addition to the above listed attributes, a number of "optional" attributes may be specified.
236
- # * For a general list of optional study level attributes, refer to the DICOM standard, PS3.4 C.6.2.1.2, Table C.6-5.
210
+ # In addition to the above listed attributes, a number of "optional" attributes
211
+ # may be specified. For a general list of optional object instance level attributes,
212
+ # please refer to the DICOM standard, PS3.4 C.6.2.1.2, Table C.6-5.
237
213
  #
238
- # === Examples
214
+ # @note Caution: Calling this method without parameters will instruct your PACS
215
+ # to return info on ALL studies in the database!
216
+ # @param [Hash] query_params the query parameters to use
217
+ # @option query_params [String] 'GGGG,EEEE' a tag and value pair to be used in the query
239
218
  #
240
- # node.find_studies("0008,0020" => "20090604-", "0010,000D" => "123456789")
219
+ # @example Find all studies matching the given study date and patient's id
220
+ # node.find_studies('0008,0020' => '20090604-', '0010,0020' => '123456789')
241
221
  #
242
222
  def find_studies(query_params={})
243
223
  # Study Root Query/Retrieve Information Model - FIND:
@@ -266,25 +246,22 @@ module DICOM
266
246
 
267
247
  # Retrieves a DICOM file from a service class provider (SCP/PACS).
268
248
  #
269
- # === Restrictions
270
- #
271
- # * This method has never actually been tested, and as such, it is probably not working! Feedback is welcome.
249
+ # === Instance level attributes for this procedure:
272
250
  #
273
- # === Parameters
251
+ # * '0008,0018' (SOP Instance UID)
252
+ # * '0008,0052' (Query/Retrieve Level)
253
+ # * '0020,000D' (Study Instance UID)
254
+ # * '0020,000E' (Series Instance UID)
274
255
  #
275
- # * <tt>path</tt> -- The path where incoming files will be saved.
276
- # * <tt>options</tt> -- A hash of parameters.
256
+ # @note This method has never actually been tested, and as such,
257
+ # it is probably not working! Feedback is welcome.
277
258
  #
278
- # === Options
259
+ # @param [String] path the directory where incoming files will be saved
260
+ # @param [Hash] options the options to use for retrieving the DICOM object
261
+ # @option options [String] 'GGGG,EEEE' a tag and value pair to be used for the procedure
279
262
  #
280
- # * <tt>"0008,0018"</tt> -- SOP Instance UID
281
- # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
282
- # * <tt>"0020,000D"</tt> -- Study Instance UID
283
- # * <tt>"0020,000E"</tt> -- Series Instance UID
284
- #
285
- # === Examples
286
- #
287
- # node.get_image("c:/dicom/", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
263
+ # @example Retrieve a file as specified by its UIDs
264
+ # node.get_image('c:/dicom/', '0008,0018' => sop_uid, '0020,000D' => study_uid, '0020,000E' => series_uid)
288
265
  #
289
266
  def get_image(path, options={})
290
267
  # Study Root Query/Retrieve Information Model - GET:
@@ -299,25 +276,22 @@ module DICOM
299
276
 
300
277
  # Moves a single image to a DICOM server.
301
278
  #
302
- # === Notes
303
- #
304
- # * This DICOM node must be a third party (not yourself).
305
- #
306
- # === Parameters
307
- #
308
- # * <tt>destination</tt> -- String. The name (AE title) of the DICOM server which will receive the file.
309
- # * <tt>options</tt> -- A hash of parameters.
279
+ # This DICOM node must be a third party (i.e. not the client instance you
280
+ # are requesting the move with!).
310
281
  #
311
- # === Options
282
+ # === Instance level attributes for this procedure:
312
283
  #
313
- # * <tt>"0008,0018"</tt> -- SOP Instance UID
314
- # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
315
- # * <tt>"0020,000D"</tt> -- Study Instance UID
316
- # * <tt>"0020,000E"</tt> -- Series Instance UID
284
+ # * '0008,0018' (SOP Instance UID)
285
+ # * '0008,0052' (Query/Retrieve Level)
286
+ # * '0020,000D' (Study Instance UID)
287
+ # * '0020,000E' (Series Instance UID)
317
288
  #
318
- # === Examples
289
+ # @param [String] destination the AE title of the DICOM server which will receive the file
290
+ # @param [Hash] options the options to use for moving the DICOM object
291
+ # @option options [String] 'GGGG,EEEE' a tag and value pair to be used for the procedure
319
292
  #
320
- # node.move_image("SOME_SERVER", "0008,0018" => sop_uid, "0020,000D" => study_uid, "0020,000E" => series_uid)
293
+ # @example Move an image from e.q. a PACS to another SCP on the network
294
+ # node.move_image('SOME_SERVER', '0008,0018' => sop_uid, '0020,000D' => study_uid, '0020,000E' => series_uid)
321
295
  #
322
296
  def move_image(destination, options={})
323
297
  # Study Root Query/Retrieve Information Model - MOVE:
@@ -332,24 +306,21 @@ module DICOM
332
306
 
333
307
  # Move an entire study to a DICOM server.
334
308
  #
335
- # === Notes
336
- #
337
- # * This DICOM node must be a third party (not yourself).
309
+ # This DICOM node must be a third party (i.e. not the client instance you
310
+ # are requesting the move with!).
338
311
  #
339
- # === Parameters
312
+ # === Instance level attributes for this procedure:
340
313
  #
341
- # * <tt>destination</tt> -- String. The name (AE title) of the DICOM server which will receive the files.
342
- # * <tt>options</tt> -- A hash of parameters.
314
+ # * '0008,0052' (Query/Retrieve Level)
315
+ # * '0010,0020' (Patient ID)
316
+ # * '0020,000D' (Study Instance UID)
343
317
  #
344
- # === Options
318
+ # @param [String] destination the AE title of the DICOM server which will receive the files
319
+ # @param [Hash] options the options to use for moving the DICOM objects
320
+ # @option options [String] 'GGGG,EEEE' a tag and value pair to be used for the procedure
345
321
  #
346
- # * <tt>"0008,0052"</tt> -- Query/Retrieve Level
347
- # * <tt>"0010,0020"</tt> -- Patient ID
348
- # * <tt>"0020,000D"</tt> -- Study Instance UID
349
- #
350
- # === Examples
351
- #
352
- # node.move_study("SOME_SERVER", "0010,0020" => pat_id, "0020,000D" => study_uid)
322
+ # @example Move an entire study from e.q. a PACS to another SCP on the network
323
+ # node.move_study('SOME_SERVER', '0010,0020' => pat_id, '0020,000D' => study_uid)
353
324
  #
354
325
  def move_study(destination, options={})
355
326
  # Study Root Query/Retrieve Information Model - MOVE:
@@ -364,13 +335,9 @@ module DICOM
364
335
 
365
336
  # Sends one or more DICOM files to a service class provider (SCP/PACS).
366
337
  #
367
- # === Parameters
368
- #
369
- # * <tt>files</tt> -- A single file path or an array of paths, or a DObject or an array of DObject instances.
370
- #
371
- # === Examples
372
- #
373
- # node.send("my_file.dcm")
338
+ # @param [Array<String, DObject>, String, DObject] files a single file path or an array of paths, alternatively a DObject or an array of DObject instances
339
+ # @example Send a DICOM file to a storage server
340
+ # node.send('my_file.dcm')
374
341
  #
375
342
  def send(files)
376
343
  # Prepare the DICOM object(s):
@@ -393,7 +360,8 @@ module DICOM
393
360
  end
394
361
  end
395
362
 
396
- # Tests the connection to the server in a very simple way by negotiating an association and then releasing it.
363
+ # Tests the connection to the server in a very simple way by negotiating
364
+ # an association and then releasing it.
397
365
  #
398
366
  def test
399
367
  logger.info("TESTING CONNECTION...")
@@ -418,7 +386,6 @@ module DICOM
418
386
  end
419
387
 
420
388
 
421
- # Following methods are private:
422
389
  private
423
390
 
424
391
 
@@ -748,18 +715,15 @@ module DICOM
748
715
  else
749
716
  # We still consider the request 'approved' if at least one context were accepted:
750
717
  @request_approved = true if @approved_syntaxes.length > 0
751
-
752
718
  logger.error("One or more of your presentation contexts were denied by host #{@host_ae}!")
753
-
754
719
  @approved_syntaxes.each_pair do |key, value|
755
- sntx_k = LIBRARY.get_syntax_description(key)
756
- sntx_v = LIBRARY.get_syntax_description(value[1])
720
+ sntx_k = (LIBRARY.uid(key) ? LIBRARY.uid(key).name : 'Unknown UID!')
721
+ sntx_v = (LIBRARY.uid(value[1]) ? LIBRARY.uid(value[1]).name : 'Unknown UID!')
757
722
  logger.info("APPROVED: #{sntx_k} (#{sntx_v})")
758
723
  end
759
-
760
724
  rejected.each_pair do |key, value|
761
- sntx_k = LIBRARY.get_syntax_description(key)
762
- sntx_v = LIBRARY.get_syntax_description(value[1])
725
+ sntx_k = (LIBRARY.uid(key) ? LIBRARY.uid(key).name : 'Unknown UID!')
726
+ sntx_v = (LIBRARY.uid(value[1]) ? LIBRARY.uid(value[1]).name : 'Unknown UID!')
763
727
  logger.error("REJECTED: #{sntx_k} (#{sntx_v})")
764
728
  end
765
729
  end
@@ -918,23 +882,31 @@ module DICOM
918
882
  def set_user_information_array
919
883
  @user_information = [
920
884
  [ITEM_MAX_LENGTH, "UL", @max_package_size],
921
- [ITEM_IMPLEMENTATION_UID, "STR", UID],
885
+ [ITEM_IMPLEMENTATION_UID, "STR", UID_ROOT],
922
886
  [ITEM_IMPLEMENTATION_VERSION, "STR", NAME]
923
887
  ]
924
888
  end
925
889
 
890
+ # Checks if an association has been established.
891
+ #
926
892
  def association_established?
927
893
  @association == true
928
894
  end
929
895
 
896
+ # Checks if a request has been approved.
897
+ #
930
898
  def request_approved?
931
899
  @request_approved == true
932
900
  end
933
901
 
902
+ # Extracts the presentation context id from the approved syntax.
903
+ #
934
904
  def presentation_context_id
935
905
  @approved_syntaxes.to_a.first[1][0] # ID of first (and only) syntax in this Hash.
936
906
  end
937
907
 
908
+ # Sets the data_elements instance array with the given options.
909
+ #
938
910
  def set_data_elements(options)
939
911
  @data_elements = []
940
912
  options.keys.sort.each do |tag|
@@ -1,288 +1,220 @@
1
- module DICOM
2
-
3
- # This class contains methods that interact with Ruby DICOM's dictionary.
4
- #
5
- class DLibrary
6
-
7
- # A hash with element name strings as key and method name symbols as value.
8
- attr_reader :methods_from_names
9
- # A hash with element method name symbols as key and name strings as value.
10
- attr_reader :names_from_methods
11
- # A hash containing tags as key and an array as value, where the array contains data element vr and name.
12
- attr_reader :tags
13
- # A hash containing UIDs as key and an array as value, where the array contains name and type.
14
- attr_reader :uid
15
-
16
- # Creates a DLibrary instance.
17
- #
18
- def initialize
19
- # Load the data elements hash, where the keys are tag strings, and values
20
- # are two-element arrays [vr, name] (where vr itself is an array of 1-3 elements):
21
- @tags = Dictionary.load_data_elements
22
- # Load UID hash (DICOM unique identifiers), where the keys are UID strings,
23
- # and values are two-element arrays [description, type]:
24
- @uid = Dictionary.load_uid
25
- create_method_conversion_tables
26
- end
27
-
28
- # Returns the method (symbol) corresponding to the specified string value (which may represent a element tag, name or method).
29
- # Returns nil if no match is found.
30
- #
31
- def as_method(value)
32
- case true
33
- when value.tag?
34
- name, vr = get_name_vr(value)
35
- @methods_from_names[name]
36
- when value.dicom_name?
37
- @methods_from_names[value]
38
- when value.dicom_method?
39
- @names_from_methods.has_key?(value.to_sym) ? value.to_sym : nil
40
- else
41
- nil
42
- end
43
- end
44
-
45
- # Returns the name (string) corresponding to the specified string value (which may represent a element tag, name or method).
46
- # Returns nil if no match is found.
47
- #
48
- def as_name(value)
49
- case true
50
- when value.tag?
51
- name, vr = get_name_vr(value)
52
- name
53
- when value.dicom_name?
54
- @methods_from_names.has_key?(value) ? value.to_s : nil
55
- when value.dicom_method?
56
- @names_from_methods[value.to_sym]
57
- else
58
- nil
59
- end
60
- end
61
-
62
- # Returns the tag (string) corresponding to the specified string value (which may represent a element tag, name or method).
63
- # Returns nil if no match is found.
64
- #
65
- def as_tag(value)
66
- case true
67
- when value.tag?
68
- name, vr = get_name_vr(value)
69
- name.nil? ? nil : value
70
- when value.dicom_name?
71
- get_tag(value)
72
- when value.dicom_method?
73
- get_tag(@names_from_methods[value.to_sym])
74
- else
75
- nil
76
- end
77
- end
78
-
79
- # Checks whether a given string is a valid transfer syntax or not.
80
- # Returns true if valid, false if not.
81
- #
82
- # === Parameters
83
- #
84
- # * <tt>uid</tt> -- String. A DICOM UID value which will be matched against known transfer syntaxes.
85
- #
86
- def check_ts_validity(uid)
87
- result = false
88
- value = @uid[uid]
89
- if value
90
- result = true if value[1] == "Transfer Syntax"
91
- end
92
- return result
93
- end
94
-
95
- # Extracts, and returns, all transfer syntaxes and SOP Classes from the dictionary,
96
- # in the form of a transfer syntax hash and a sop class hash.
97
- #
98
- # Both hashes have UIDs as keys and their descriptions as values.
99
- #
100
- def extract_transfer_syntaxes_and_sop_classes
101
- transfer_syntaxes = Hash.new
102
- sop_classes = Hash.new
103
- @uid.each_pair do |key, value|
104
- if value[1] == "Transfer Syntax"
105
- transfer_syntaxes[key] = value[0]
106
- elsif value[1] == "SOP Class"
107
- sop_classes[key] = value[0]
108
- end
109
- end
110
- return transfer_syntaxes, sop_classes
111
- end
112
-
113
- # Checks if the specified transfer syntax implies the presence of pixel compression.
114
- # Returns true if pixel compression is implied, false if not.
115
- #
116
- # === Parameters
117
- #
118
- # * <tt>uid</tt> -- String. A DICOM UID value.
119
- #
120
- def get_compression(uid)
121
- raise ArgumentError, "Expected String, got #{uid.class}" unless uid.is_a?(String)
122
- result = false
123
- value = @uid[uid]
124
- if value
125
- first_word = value[0].split(" ").first
126
- result = true if value[1] == "Transfer Syntax" and not ["Implicit", "Explicit"].include?(first_word)
127
- end
128
- return result
129
- end
130
-
131
- # Determines, and returns, the name and vr of the data element which the specified tag belongs to.
132
- # Values are retrieved from the Ruby DICOM dictionary if a match is found.
133
- #
134
- # === Notes
135
- #
136
- # * Private tags will have their names listed as "Private".
137
- # * Non-private tags that are not found in the dictionary will be listed as "Unknown".
138
- #
139
- # === Parameters
140
- #
141
- # * <tt>tag</tt> -- String. A data element tag.
142
- #
143
- def get_name_vr(tag)
144
- if tag.private? and tag.element != GROUP_LENGTH
145
- name = "Private"
146
- vr = "UN"
147
- else
148
- # Check the dictionary:
149
- values = @tags[tag]
150
- if values
151
- name = values[1]
152
- vr = values[0][0]
153
- else
154
- # For the tags that are not recognised, we need to do some additional testing to see if it is one of the special cases:
155
- if tag.element == GROUP_LENGTH
156
- # Group length:
157
- name = "Group Length"
158
- vr = "UL"
159
- elsif tag[0..6] == "0020,31"
160
- # Source Image ID's (Retired):
161
- values = @tags["0020,31xx"]
162
- name = values[1]
163
- vr = values[0][0]
164
- elsif tag.group == "1000" and tag.element =~ /\A\h{3}[0-5]\z/
165
- # Group 1000,xxx[0-5] (Retired):
166
- new_tag = tag.group + "xx" + tag.element[3..3]
167
- values = @tags[new_tag]
168
- elsif tag.group == "1010"
169
- # Group 1010,xxxx (Retired):
170
- new_tag = tag.group + "xxxx"
171
- values = @tags[new_tag]
172
- elsif tag[0..1] == "50" or tag[0..1] == "60"
173
- # Group 50xx (Retired) and 60xx:
174
- new_tag = tag[0..1]+"xx"+tag[4..8]
175
- values = @tags[new_tag]
176
- if values
177
- name = values[1]
178
- vr = values[0][0]
179
- end
180
- elsif tag[0..1] == "7F" and tag[5..6] == "00"
181
- # Group 7Fxx,00[10,11,20,30,40] (Retired):
182
- new_tag = tag[0..1]+"xx"+tag[4..8]
183
- values = @tags[new_tag]
184
- if values
185
- name = values[1]
186
- vr = values[0][0]
187
- end
188
- end
189
- # If none of the above checks yielded a result, the tag is unknown:
190
- unless name
191
- name = "Unknown"
192
- vr = "UN"
193
- end
194
- end
195
- end
196
- return name, vr
197
- end
198
-
199
- # Returns the tag that matches the supplied data element name, by searching the Ruby DICOM dictionary.
200
- # Returns nil if no match is found.
201
- #
202
- # === Parameters
203
- #
204
- # * <tt>name</tt> -- String. A data element name.
205
- #
206
- def get_tag(name)
207
- tag = nil
208
- name = name.to_s.downcase
209
- @tag_name_pairs_cache ||= Hash.new
210
- return @tag_name_pairs_cache[name] unless @tag_name_pairs_cache[name].nil?
211
- @tags.each_pair do |key, value|
212
- next unless value[1].downcase == name
213
- tag = key
214
- break
215
- end
216
- @tag_name_pairs_cache[name]=tag
217
- return tag
218
- end
219
-
220
- # Returns the description/name of a specified UID (i.e. a transfer syntax or SOP class).
221
- # Returns nil if no match is found
222
- #
223
- # === Parameters
224
- #
225
- # * <tt>uid</tt> -- String. A DICOM UID value.
226
- #
227
- def get_syntax_description(uid)
228
- name = nil
229
- value = @uid[uid]
230
- name = value[0] if value
231
- return name
232
- end
233
-
234
- # Checks the validity of the specified transfer syntax UID and determines the
235
- # encoding settings (explicitness & endianness) associated with this value.
236
- # The results are returned as 3 booleans: validity, explicitness & endianness.
237
- #
238
- # === Parameters
239
- #
240
- # * <tt>uid</tt> -- String. A DICOM UID value.
241
- #
242
- def process_transfer_syntax(uid)
243
- valid = check_ts_validity(uid)
244
- case uid
245
- # Some variations with uncompressed pixel data:
246
- when IMPLICIT_LITTLE_ENDIAN
247
- explicit = false
248
- endian = false
249
- when EXPLICIT_LITTLE_ENDIAN
250
- explicit = true
251
- endian = false
252
- when "1.2.840.10008.1.2.1.99" # Deflated Explicit VR, Little Endian
253
- # Note: Has this transfer syntax been tested yet?
254
- explicit = true
255
- endian = false
256
- when EXPLICIT_BIG_ENDIAN
257
- explicit = true
258
- endian = true
259
- else
260
- # For everything else, assume compressed pixel data, with Explicit VR, Little Endian:
261
- explicit = true
262
- endian = false
263
- end
264
- return valid, explicit, endian
265
- end
266
-
267
-
268
- private
269
-
270
-
271
- # Creates the instance hashes that are used for name to/from method conversion.
272
- #
273
- def create_method_conversion_tables
274
- if @methods_from_names.nil?
275
- @methods_from_names = Hash.new
276
- @names_from_methods = Hash.new
277
- # Fill the hashes:
278
- @tags.each_pair do |key, value|
279
- name = value[1]
280
- method_name = name.dicom_methodize
281
- @methods_from_names[name] = method_name.to_sym
282
- @names_from_methods[method_name.to_sym] = name
283
- end
284
- end
285
- end
286
-
287
- end
1
+ module DICOM
2
+
3
+ # The DLibrary class contains methods for interacting with ruby-dicom's dictionary data.
4
+ #
5
+ # In practice, the library is for internal use and not accessed by the user. However, a
6
+ # a library instance is available through the DICOM::LIBRARY constant.
7
+ #
8
+ # @example Get a dictionary element corresponding to the given tag
9
+ # element = DICOM::LIBRARY.element('0010,0010')
10
+ #
11
+ class DLibrary
12
+
13
+ # A hash with element name strings as key and method name symbols as value.
14
+ attr_reader :methods_from_names
15
+ # A hash with element method name symbols as key and name strings as value.
16
+ attr_reader :names_from_methods
17
+
18
+ # Creates a DLibrary instance.
19
+ #
20
+ def initialize
21
+ # Create instance hashes used for dictionary data and method conversion:
22
+ @elements = Hash.new
23
+ @uids = Hash.new
24
+ @methods_from_names = Hash.new
25
+ @names_from_methods = Hash.new
26
+ # Load the elements dictionary:
27
+ File.open("#{ROOT_DIR}/dictionary/elements.txt").each do |record|
28
+ load_element(record)
29
+ end
30
+ # Load the unique identifiers dictionary:
31
+ File.open("#{ROOT_DIR}/dictionary/uids.txt").each do |record|
32
+ fields = record.split("\t")
33
+ # Store the uids in a hash with uid-value as key and the uid instance as value:
34
+ @uids[fields[0]] = UID.new(fields[0], fields[1], fields[2].rstrip, fields[3].rstrip)
35
+ end
36
+ end
37
+
38
+ # Adds a custom dictionary file to the ruby-dicom element dictionary.
39
+ #
40
+ # @note The format of the dictionary is a tab-separated text file with 5 columns:
41
+ # * Tag, Name, VR, VM & Retired status
42
+ # * For samples check out ruby-dicom's element dictionaries in the git repository
43
+ # @param [String] file The path to the dictionary file to be added
44
+ #
45
+ def add_element_dictionary(file)
46
+ File.open(file).each do |record|
47
+ load_element(record)
48
+ end
49
+ end
50
+
51
+
52
+ # Gives the method (symbol) corresponding to the specified element string value.
53
+ #
54
+ # @param [String] value an element tag, element name or an element's method name
55
+ # @return [Symbol, NilClass] the matched element method, or nil if no match is made
56
+ #
57
+ def as_method(value)
58
+ case true
59
+ when value.tag?
60
+ @methods_from_names[element(value).name]
61
+ when value.dicom_name?
62
+ @methods_from_names[value]
63
+ when value.dicom_method?
64
+ @names_from_methods.has_key?(value.to_sym) ? value.to_sym : nil
65
+ else
66
+ nil
67
+ end
68
+ end
69
+
70
+ # Gives the name corresponding to the specified element string value.
71
+ #
72
+ # @param [String] value an element tag, element name or an element's method name
73
+ # @return [String, NilClass] the matched element name, or nil if no match is made
74
+ #
75
+ def as_name(value)
76
+ case true
77
+ when value.tag?
78
+ element(value).name
79
+ when value.dicom_name?
80
+ @methods_from_names.has_key?(value) ? value.to_s : nil
81
+ when value.dicom_method?
82
+ @names_from_methods[value.to_sym]
83
+ else
84
+ nil
85
+ end
86
+ end
87
+
88
+ # Gives the tag corresponding to the specified element string value.
89
+ #
90
+ # @param [String] value an element tag, element name or an element's method name
91
+ # @return [String, NilClass] the matched element tag, or nil if no match is made
92
+ #
93
+ def as_tag(value)
94
+ case true
95
+ when value.tag?
96
+ element(value) ? value : nil
97
+ when value.dicom_name?
98
+ get_tag(value)
99
+ when value.dicom_method?
100
+ get_tag(@names_from_methods[value.to_sym])
101
+ else
102
+ nil
103
+ end
104
+ end
105
+
106
+ # Identifies the DictionaryElement that corresponds to the given tag.
107
+ #
108
+ # @note If a given tag doesn't return a dictionary match, a new DictionaryElement is created.
109
+ # * For private tags, a name 'Private' and VR 'UN' is assigned
110
+ # * For unknown tags, a name 'Unknown' and VR 'UN' is assigned
111
+ # @param [String] tag the tag of the element
112
+ # @return [DictionaryElement] a corresponding DictionaryElement
113
+ #
114
+ def element(tag)
115
+ element = @elements[tag]
116
+ unless element
117
+ if tag.group_length?
118
+ element = DictionaryElement.new(tag, 'Group Length', ['UL'], '1', '')
119
+ else
120
+ if tag.private?
121
+ element = DictionaryElement.new(tag, 'Private', ['UN'], '1', '')
122
+ else
123
+ if !(de = @elements["#{tag[0..3]},xxx#{tag[8]}"]).nil? # 1000,xxxh
124
+ element = DictionaryElement.new(tag, de.name, de.vrs, de.vm, de.retired)
125
+ elsif !(de = @elements["#{tag[0..3]},xxxx"]).nil? # 1010,xxxx
126
+ element = DictionaryElement.new(tag, de.name, de.vrs, de.vm, de.retired)
127
+ elsif !(de = @elements["#{tag[0..1]}xx,#{tag[5..8]}"]).nil? # hhxx,hhhh
128
+ element = DictionaryElement.new(tag, de.name, de.vrs, de.vm, de.retired)
129
+ elsif !(de = @elements["#{tag[0..6]}x#{tag[8]}"]).nil? # 0028,hhxh
130
+ element = DictionaryElement.new(tag, de.name, de.vrs, de.vm, de.retired)
131
+ else
132
+ # We are facing an unknown (but not private) tag:
133
+ element = DictionaryElement.new(tag, 'Unknown', ['UN'], '1', '')
134
+ end
135
+ end
136
+ end
137
+ end
138
+ return element
139
+ end
140
+
141
+ # Extracts, and returns, all transfer syntaxes and SOP Classes from the dictionary.
142
+ #
143
+ # @return [Array<Hash, Hash>] transfer syntax and sop class hashes, each with uid as key and name as value
144
+ #
145
+ def extract_transfer_syntaxes_and_sop_classes
146
+ transfer_syntaxes = Hash.new
147
+ sop_classes = Hash.new
148
+ @uids.each_value do |uid|
149
+ if uid.transfer_syntax?
150
+ transfer_syntaxes[uid.value] = uid.name
151
+ elsif uid.sop_class?
152
+ sop_classes[uid.value] = uid.name
153
+ end
154
+ end
155
+ return transfer_syntaxes, sop_classes
156
+ end
157
+
158
+ # Gives the tag that matches the supplied data element name, by searching the element dictionary.
159
+ #
160
+ # @param [String] name a data element name
161
+ # @return [String, NilClass] the corresponding element tag, or nil if no match is made
162
+ #
163
+ def get_tag(name)
164
+ tag = nil
165
+ name = name.to_s.downcase
166
+ @tag_name_pairs_cache ||= Hash.new
167
+ return @tag_name_pairs_cache[name] unless @tag_name_pairs_cache[name].nil?
168
+ @elements.each_value do |element|
169
+ next unless element.name.downcase == name
170
+ tag = element.tag
171
+ break
172
+ end
173
+ @tag_name_pairs_cache[name]=tag
174
+ return tag
175
+ end
176
+
177
+ # Determines the name and vr of the element which the specified tag belongs to,
178
+ # based on a lookup in the element data dictionary.
179
+ #
180
+ # @note If a given tag doesn't return a dictionary match, the following values are assigned:
181
+ # * For private tags: name 'Private' and VR 'UN'
182
+ # * For unknown (non-private) tags: name 'Unknown' and VR 'UN'
183
+ # @param [String] tag an element's tag
184
+ # @return [Array<String, String>] the name and value representation corresponding to the given tag
185
+ #
186
+ def name_and_vr(tag)
187
+ de = element(tag)
188
+ return de.name, de.vr
189
+ end
190
+
191
+ # Identifies the UID that corresponds to the given value.
192
+ #
193
+ # @param [String] value the unique identifier value
194
+ # @return [UID, NilClass] a corresponding UID instance, or nil (if no match is made)
195
+ #
196
+ def uid(value)
197
+ @uids[value]
198
+ end
199
+
200
+
201
+ private
202
+
203
+
204
+ # Loads an element to the dictionary from an element string record.
205
+ #
206
+ # @param [String] record a tab-separated string line as extracted from a dictionary file
207
+ #
208
+ def load_element(record)
209
+ fields = record.split("\t")
210
+ # Store the elements in a hash with tag as key and the element instance as value:
211
+ element = DictionaryElement.new(fields[0], fields[1], fields[2].split(","), fields[3].rstrip, fields[4].rstrip)
212
+ @elements[fields[0]] = element
213
+ # Populate the method conversion hashes with element data:
214
+ method = element.name.to_element_method
215
+ @methods_from_names[element.name] = method
216
+ @names_from_methods[method] = element.name
217
+ end
218
+
219
+ end
288
220
  end