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.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://www.rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path('../lib/dicom/version', __FILE__)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.platform = Gem::Platform::RUBY
7
+ s.name = 'dicom'
8
+ s.version = DICOM::VERSION
9
+ s.date = Time.now
10
+ s.summary = "Library for handling DICOM files and DICOM network communication."
11
+ s.require_paths = ['lib']
12
+ s.author = "Christoffer Lervag"
13
+ s.email = "chris.lervag@gmail.com"
14
+ s.homepage = "http://dicom.rubyforge.org/"
15
+ s.license = "GPLv3"
16
+ s.description = "DICOM is a standard widely used throughout the world to store and transfer medical image data. This library enables efficient and powerful handling of DICOM in Ruby, to the benefit of any student or professional who would like to use their favorite language to process DICOM files and communicate across the network."
17
+ s.files = Dir["{lib}/**/*", "[A-Z]*"]
18
+ s.rubyforge_project = 'dicom'
19
+
20
+ s.required_ruby_version = '>= 1.9.2'
21
+ s.required_rubygems_version = '>= 1.8.6'
22
+
23
+ s.add_development_dependency('bundler', '>= 1.0.0')
24
+ s.add_development_dependency('rake', '>= 0.9.2.2')
25
+ s.add_development_dependency('rspec', '>= 2.9.0')
26
+ s.add_development_dependency('mocha', '>= 0.10.5')
27
+ s.add_development_dependency('narray', '>= 0.6.0.0')
28
+ s.add_development_dependency('rmagick', '>= 2.12.0')
29
+ s.add_development_dependency('mini_magick', '>= 3.2.1')
30
+ s.add_development_dependency('yard', '>= 0.8.2')
31
+ end
@@ -1,51 +1,53 @@
1
- # Loads the files that are used by Ruby DICOM.
2
- #
3
- # The following classes are meant to be used by users of Ruby DICOM:
4
- # * DObject - for reading, manipulating and writing DICOM files.
5
- # * Element, Sequence, Item, Parent, Elemental - users who wish to interact with
6
- # their DICOM objects will use these classes/modules.
7
- # * ImageItem - Image related methods are found in this class.
8
- # * DClient - for client side network communication, like querying, moving & sending DICOM files.
9
- # * DServer - for server side network communication: Setting up your own DICOM storage node (SCP).
10
- # * Anonymizer - a convenience class for anonymizing your DICOM files.
11
- #
12
- # The rest of the classes visible in the documentation generated by RDoc are
13
- # in principle 'private' classes, which are mainly of interest to developers.
14
-
15
- # Logging:
16
- require_relative 'dicom/logging'
17
- # Core library:
18
- # Super classes/modules:
19
- require_relative 'dicom/image_processor'
20
- require_relative 'dicom/parent'
21
- require_relative 'dicom/image_item'
22
- require_relative 'dicom/elemental'
23
- # Subclasses and independent classes:
24
- require_relative 'dicom/d_client'
25
- require_relative 'dicom/dictionary'
26
- require_relative 'dicom/d_library'
27
- require_relative 'dicom/d_object'
28
- require_relative 'dicom/d_read'
29
- require_relative 'dicom/d_server'
30
- require_relative 'dicom/d_write'
31
- require_relative 'dicom/element'
32
- require_relative 'dicom/file_handler'
33
- require_relative 'dicom/item'
34
- require_relative 'dicom/link'
35
- require_relative 'dicom/sequence'
36
- require_relative 'dicom/stream'
37
- # Extensions to the Ruby library:
38
- require_relative 'dicom/ruby_extensions'
39
- # Module settings:
40
- require_relative 'dicom/version'
41
- require_relative 'dicom/constants'
42
- require_relative 'dicom/variables'
43
- # Image processors:
44
- require_relative 'dicom/image_processor_mini_magick'
45
- require_relative 'dicom/image_processor_r_magick'
46
- # Deprecated methods:
47
- require_relative 'dicom/deprecated'
48
-
49
- # Extensions (non-core functionality):
50
- require_relative 'dicom/anonymizer'
51
- require_relative 'dicom/audit_trail'
1
+ # Loads the files that are used by ruby-dicom.
2
+ #
3
+ # The following classes are meant to be used by users of ruby-dicom:
4
+ # * DObject - for reading, manipulating and writing DICOM files.
5
+ # * Element, Sequence, Item, Parent, Elemental - users who wish to interact with
6
+ # their DICOM objects will use these classes/modules.
7
+ # * ImageItem - Image related methods are found in this class.
8
+ # * DClient - for client side network communication, like querying, moving & sending DICOM files.
9
+ # * DServer - for server side network communication: Setting up your own DICOM storage node (SCP).
10
+ # * Anonymizer - a convenience class for anonymizing your DICOM files.
11
+ #
12
+ # The rest of the classes visible in the documentation generated by YARD are
13
+ # in principle 'private' classes, which are mainly of interest to developers.
14
+
15
+ # Logging:
16
+ require_relative 'dicom/logging'
17
+ # Core library:
18
+ # Super classes/modules:
19
+ require_relative 'dicom/image_processor'
20
+ require_relative 'dicom/parent'
21
+ require_relative 'dicom/image_item'
22
+ require_relative 'dicom/elemental'
23
+ # Subclasses and independent classes:
24
+ require_relative 'dicom/d_client'
25
+ require_relative 'dicom/d_object'
26
+ require_relative 'dicom/d_read'
27
+ require_relative 'dicom/d_server'
28
+ require_relative 'dicom/d_write'
29
+ require_relative 'dicom/element'
30
+ require_relative 'dicom/file_handler'
31
+ require_relative 'dicom/item'
32
+ require_relative 'dicom/link'
33
+ require_relative 'dicom/sequence'
34
+ require_relative 'dicom/stream'
35
+ # Dictionary:
36
+ require_relative 'dicom/d_library'
37
+ require_relative 'dicom/dictionary_element'
38
+ require_relative 'dicom/uid'
39
+ # Extensions to the Ruby library:
40
+ require_relative 'dicom/ruby_extensions'
41
+ # Module settings:
42
+ require_relative 'dicom/version'
43
+ require_relative 'dicom/constants'
44
+ require_relative 'dicom/variables'
45
+ # Image processors:
46
+ require_relative 'dicom/image_processor_mini_magick'
47
+ require_relative 'dicom/image_processor_r_magick'
48
+ # Deprecated methods:
49
+ require_relative 'dicom/deprecated'
50
+
51
+ # Extensions (non-core functionality):
52
+ require_relative 'dicom/anonymizer'
53
+ require_relative 'dicom/audit_trail'
@@ -1,12 +1,11 @@
1
1
  module DICOM
2
2
 
3
- # This is a convenience class for handling anonymization of DICOM files.
3
+ # This is a convenience class for handling the anonymization (de-identification) of DICOM files.
4
4
  #
5
- # === Notes
6
- #
7
- # For 'advanced' anonymization, a good resource might be:
8
- # ftp://medical.nema.org/medical/dicom/supps/sup142_pc.pdf
9
- # (Clinical Trials De-identification Profiles, DICOM Standards Committee, Working Group 18)
5
+ # @note
6
+ # For 'advanced' anonymization, a good resource might be the work on "Clinical Trials
7
+ # De-identification Profiles" by the DICOM Standards Committee, Working Group 18:
8
+ # ftp://medical.nema.org/medical/dicom/supps/sup142_pc.pdf
10
9
  #
11
10
  class Anonymizer
12
11
  include Logging
@@ -34,26 +33,15 @@ module DICOM
34
33
 
35
34
  # Creates an Anonymizer instance.
36
35
  #
37
- # === Notes
38
- #
39
- # * To customize logging behaviour, refer to the Logging module documentation.
40
- #
41
- # === Parameters
42
- #
43
- # * <tt>options</tt> -- A hash of parameters.
44
- #
45
- # === Options
46
- #
47
- # * <tt>:audit_trail</tt> -- String. A file name path. If the file contains old audit data, these are loaded and used in the current anonymization.
48
- # * <tt>:uid</tt> -- Boolean. If true, all (top level) UIDs will be replaced with custom generated UIDs. To preserve UID relations in studies/series, the AuditTrail feature must be used.
49
- # * <tt>:uid_root</tt> -- String. An organization (or custom) UID root to use when replacing UIDs.
50
- #
51
- # === Examples
52
- #
53
- # # Create an Anonymizer instance and restrict the log output:
36
+ # @note To customize logging behaviour, refer to the Logging module documentation.
37
+ # @param [Hash] options the options to create an anonymizer instance with
38
+ # @option options [String] :audit_trail a file name path. If the file contains old audit data, these are loaded and used in the current anonymization.
39
+ # @option options [Boolean] :uid if true, all (top level) UIDs will be replaced with custom generated UIDs. To preserve UID relations in studies/series, the AuditTrail feature must be used.
40
+ # @option options [String] :uid_root an organization (or custom) UID root to use when replacing UIDs.
41
+ # @example Create an Anonymizer instance and restrict the log output
54
42
  # a = Anonymizer.new
55
43
  # a.logger.level = Logger::ERROR
56
- # # Carry out anonymization using the audit trail feature:
44
+ # @example Perform anonymization using the audit trail feature
57
45
  # a = Anonymizer.new(:audit_trail => "trail.json")
58
46
  # a.enumeration = true
59
47
  # a.folder = "//dicom/today/"
@@ -85,7 +73,7 @@ module DICOM
85
73
  # Register the uid anonymization option:
86
74
  @uid = options[:uid]
87
75
  # Set the uid_root to be used when anonymizing study_uid series_uid and sop_instance_uid
88
- @uid_root = options[:uid_root] ? options[:uid_root] : UID
76
+ @uid_root = options[:uid_root] ? options[:uid_root] : UID_ROOT
89
77
  # Setup audit trail if requested:
90
78
  if options[:audit_trail]
91
79
  @audit_trail_file = options[:audit_trail]
@@ -103,12 +91,8 @@ module DICOM
103
91
 
104
92
  # Adds an exception folder which will be avoided when anonymizing.
105
93
  #
106
- # === Parameters
107
- #
108
- # * <tt>path</tt> -- String. A path that will be avoided.
109
- #
110
- # === Examples
111
- #
94
+ # @param [String] path a path that will be avoided
95
+ # @example Adding a folder
112
96
  # a.add_exception("/home/dicom/tutorials/")
113
97
  #
114
98
  def add_exception(path)
@@ -122,12 +106,8 @@ module DICOM
122
106
 
123
107
  # Adds a folder who's files will be anonymized.
124
108
  #
125
- # === Parameters
126
- #
127
- # * <tt>path</tt> -- String. A path that will be included in the anonymization.
128
- #
129
- # === Examples
130
- #
109
+ # @param [String] path a path that will be included in the anonymization
110
+ # @example Adding a folder
131
111
  # a.add_folder("/home/dicom")
132
112
  #
133
113
  def add_folder(path)
@@ -135,12 +115,10 @@ module DICOM
135
115
  @folders << path
136
116
  end
137
117
 
138
- # Returns the enumeration status for this tag.
139
- # Returns nil if no match is found for the provided tag.
118
+ # Checks the enumeration status of this tag.
140
119
  #
141
- # === Parameters
142
- #
143
- # * <tt>tag</tt> -- String. A data element tag.
120
+ # @param [String] tag a data element tag
121
+ # @return [Boolean, NilClass] the enumeration status of the tag, or nil if the tag has no match
144
122
  #
145
123
  def enum(tag)
146
124
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
@@ -158,14 +136,10 @@ module DICOM
158
136
  #
159
137
  # This method is run when all settings have been finalized for the Anonymization instance.
160
138
  #
161
- # === Restrictions
162
- #
163
- # * Only top level data elements are anonymized!
164
- #
165
- #--
166
- # FIXME: This method has grown a bit lengthy. Perhaps it should be looked at one day.
139
+ # @note Only top level data elements are anonymized!
167
140
  #
168
141
  def execute
142
+ # FIXME: This method has grown a bit lengthy. Perhaps it should be looked at one day.
169
143
  # Search through the folders to gather all the files to be anonymized:
170
144
  logger.info("Initiating anonymization process.")
171
145
  start_time = Time.now.to_f
@@ -286,8 +260,10 @@ module DICOM
286
260
  end
287
261
 
288
262
  # Setter method for the identity file.
289
- # NB! The identity file feature is deprecated!
290
- # Please use the AuditTrail feature instead.
263
+ #
264
+ # @deprecated The identity file feature is deprecated!
265
+ # Please use the AuditTrail feature instead.
266
+ # @param [String] file_name the path of the identity file
291
267
  #
292
268
  def identity_file=(file_name)
293
269
  # Deprecation warning:
@@ -307,7 +283,7 @@ module DICOM
307
283
  type_lengths = Array.new
308
284
  value_lengths = Array.new
309
285
  @tags.each_index do |i|
310
- name, vr = LIBRARY.get_name_vr(@tags[i])
286
+ name, vr = LIBRARY.name_and_vr(@tags[i])
311
287
  names << name
312
288
  types << vr
313
289
  tag_lengths[i] = @tags[i].length
@@ -352,12 +328,8 @@ module DICOM
352
328
 
353
329
  # Removes a tag from the list of tags that will be anonymized.
354
330
  #
355
- # === Parameters
356
- #
357
- # * <tt>tag</tt> -- String. A data element tag.
358
- #
359
- # === Examples
360
- #
331
+ # @param [String] tag a data element tag
332
+ # @example Do not anonymize the Patient's Name tag
361
333
  # a.remove_tag("0010,0010")
362
334
  #
363
335
  def remove_tag(tag)
@@ -370,15 +342,11 @@ module DICOM
370
342
  @enumerations.delete_at(pos)
371
343
  end
372
344
  end
373
-
345
+
374
346
  # Compeletely deletes a tag from the file
375
347
  #
376
- # === Parameters
377
- #
378
- # * <tt>tag</tt> -- String. A data element tag.
379
- #
380
- # === Examples
381
- #
348
+ # @param [String] tag a data element tag
349
+ # @example Completely delete the Patient's Name tag from the DICOM files
382
350
  # a.delete_tag("0010,0010")
383
351
  #
384
352
  def delete_tag(tag)
@@ -390,19 +358,12 @@ module DICOM
390
358
  # Sets the anonymization settings for the specified tag. If the tag is already present in the list
391
359
  # of tags to be anonymized, its settings are updated, and if not, a new tag entry is created.
392
360
  #
393
- # === Parameters
394
- #
395
- # * <tt>tag</tt> -- String. A data element tag.
396
- # * <tt>options</tt> -- A hash of parameters.
397
- #
398
- # === Options
399
- #
400
- # * <tt>:value</tt> -- The replacement value to be used when anonymizing this data element. Defaults to the pre-existing value and "" for new tags.
401
- # * <tt>:enum</tt> -- Boolean. Specifies if enumeration is to be used for this tag. Defaults to the pre-existing value and false for new tags.
402
- #
403
- # === Examples
404
- #
405
- # a.set_tag("0010,0010, :value => "MrAnonymous", :enum => true)
361
+ # @param [String] tag a data element tag
362
+ # @param [Hash] options the anonymization settings for the specified tag
363
+ # @option options [String, Integer, Float] :value the replacement value to be used when anonymizing this data element. Defaults to the pre-existing value and "" for new tags.
364
+ # @option options [String, Integer, Float] :enum specifies if enumeration is to be used for this tag. Defaults to the pre-existing value and false for new tags.
365
+ # @example Set the anonymization settings of the Patient's Name tag
366
+ # a.set_tag("0010,0010", :value => "MrAnonymous", :enum => true)
406
367
  #
407
368
  def set_tag(tag, options={})
408
369
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
@@ -415,19 +376,18 @@ module DICOM
415
376
  else
416
377
  # Add new elements:
417
378
  @tags << tag
418
- @values << (options[:value] ? options[:value] : "")
379
+ @values << (options[:value] ? options[:value] : default_value(tag))
419
380
  @enumerations << (options[:enum] ? options[:enum] : false)
420
381
  end
421
382
  end
422
383
 
423
- # Returns the value which will be used when anonymizing this tag.
424
- # If enumeration is selected for the particular tag, a number will be
425
- # appended in addition to the string that is returned here.
426
- # Returns nil if no match is found for the provided tag.
384
+ # Gives the value which will be used when anonymizing this tag.
427
385
  #
428
- # === Parameters
386
+ # @note If enumeration is selected for a string type tag, a number will be
387
+ # appended in addition to the string that is returned here.
429
388
  #
430
- # * <tt>tag</tt> -- String. A data element tag.
389
+ # @param [String] tag a data element tag
390
+ # @return [String, Integer, Float, NilClass] the replacement value for the specified tag, or nil if the tag is not matched
431
391
  #
432
392
  def value(tag)
433
393
  raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
@@ -442,18 +402,15 @@ module DICOM
442
402
  end
443
403
 
444
404
 
445
- # The following methods are private:
446
405
  private
447
406
 
448
407
 
449
408
  # Finds the common path (if any) in the instance file path array, by performing a recursive search
450
409
  # on the folders that make up the path of one such file.
451
- # Returns the index of the last folder in the path of the selected file that is common for all file paths.
452
- #
453
- # === Parameters
454
410
  #
455
- # * <tt>str_arr</tt> -- An array of folder strings from the path of a select file.
456
- # * <tt>index</tt> -- Fixnum. The index of the folder in str_arr to check against all file paths.
411
+ # @param [Array<String>] str_arr an array of folder strings from the path of a select file
412
+ # @param [Fixnum] index the index of the folder in str_arr to check against all file paths
413
+ # @return [Fixnum] the index of the last folder in the path of the selected file that is common for all file paths
457
414
  #
458
415
  def common_path(str_arr, index=0)
459
416
  common_folders = Array.new
@@ -482,16 +439,31 @@ module DICOM
482
439
  end
483
440
  end
484
441
 
442
+ # Determines a default value to use for anonymizing the given tag.
443
+ #
444
+ # @param [String] tag a data element tag
445
+ # @return [String, Integer, Float] the default replacement value for a given tag
446
+ #
447
+ def default_value(tag)
448
+ name, vr = LIBRARY.name_and_vr(tag)
449
+ conversion = VALUE_CONVERSION[vr] || :to_s
450
+ case conversion
451
+ when :to_i then return 0
452
+ when :to_f then return 0.0
453
+ else
454
+ # Assume type is string and return an empty string:
455
+ return ""
456
+ end
457
+ end
458
+
485
459
  # Handles the enumeration for the given data element tag.
486
460
  # If its value has been encountered before, its corresponding enumerated
487
461
  # replacement value is retrieved, and if a new original value is encountered,
488
462
  # a new enumerated replacement value is found by increasing an index by 1.
489
- # Returns the replacement value which is used for the anonymization of the tag.
490
- #
491
- # === Parameters
492
463
  #
493
- # * <tt>original</tt> -- The original value of the tag that to be anonymized.
494
- # * <tt>j</tt> -- Fixnum. The index of this tag in the tag-related instance arrays.
464
+ # @param [String, Integer, Float] original the original value of the tag to be anonymized
465
+ # @param [Fixnum] j the index of this tag in the tag-related instance arrays
466
+ # @return [String, Integer, Float] the replacement value which is used for the anonymization of the tag
495
467
  #
496
468
  def enumerated_value(original, j)
497
469
  # Is enumeration requested for this tag?
@@ -503,7 +475,11 @@ module DICOM
503
475
  # This original value has not been encountered yet. Determine the index to use.
504
476
  index = @audit_trail.records(@tags[j]).length + 1
505
477
  # Create the replacement value:
506
- replacement = @values[j] + index.to_s
478
+ if @values[j].is_a?(String)
479
+ replacement = @values[j] + index.to_s
480
+ else
481
+ replacement = @values[j] + index
482
+ end
507
483
  # Add this tag record to the audit trail:
508
484
  @audit_trail.add_record(@tags[j], original, replacement)
509
485
  end
@@ -558,8 +534,8 @@ module DICOM
558
534
  end
559
535
 
560
536
  # Analyzes the write_path and the 'read' file path to determine if they have some common root.
561
- # If there are parts of the file path that exists also in the write path, the common parts will not be added to the write_path.
562
- # The processed paths are put in a write_path instance array.
537
+ # If there are parts of the file path that exists also in the write path, the common parts will
538
+ # not be added to the write_path. The processed paths are put in a write_path instance array.
563
539
  #
564
540
  def process_write_paths
565
541
  # First make sure @write_path ends with a file separator character:
@@ -594,13 +570,10 @@ module DICOM
594
570
 
595
571
  # Replaces the UIDs of the given DICOM object.
596
572
  #
597
- # === Notes
598
- #
599
- # Empty UIDs are ignored (we don't generate new UIDs for these).
600
- # If AuditTrail is set, the relationship between old and new UIDs
601
- # are preserved, and the relations between files in a study/series
602
- # should remain valid.
603
- #
573
+ # @note Empty UIDs are ignored (we don't generate new UIDs for these).
574
+ # @note If AuditTrail is set, the relationship between old and new UIDs are preserved,
575
+ # and the relations between files in a study/series should remain valid.
576
+ # @param [DObject] dcm the dicom object to be processed
604
577
  #
605
578
  def replace_uids(dcm)
606
579
  @uids.each_pair do |tag, prefix|
@@ -641,28 +614,27 @@ module DICOM
641
614
  # Sets up default tags that will be anonymized, along with default replacement values and enumeration settings.
642
615
  # This data is stored in 3 separate instance arrays for tags, values and enumeration.
643
616
  data = [
644
- ["0008,0012", "20000101", false], # Instance Creation Date
645
- ["0008,0013", "000000.00", false], # Instance Creation Time
646
- ["0008,0020", "20000101", false], # Study Date
647
- ["0008,0023", "20000101", false], # Image Date
648
- ["0008,0030", "000000.00", false], # Study Time
649
- ["0008,0033", "000000.00", false], # Image Time
650
- ["0008,0050", "", true], # Accession Number
651
- ["0008,0080", "Institution", true], # Institution name
652
- ["0008,0090", "Physician", true], # Referring Physician's name
653
- ["0008,1010", "Station", true], # Station name
654
- ["0008,1070", "Operator", true], # Operator's Name
655
- ["0010,0010", "Patient", true], # Patient's name
656
- ["0010,0020", "ID", true], # Patient's ID
657
- ["0010,0030", "20000101", false], # Patient's Birth Date
658
- ["0010,0040", "N", false], # Patient's Sex
659
- ["0020,4000", "", false], # Image Comments
617
+ ["0008,0012", "20000101", false], # Instance Creation Date
618
+ ["0008,0013", "000000.00", false], # Instance Creation Time
619
+ ["0008,0020", "20000101", false], # Study Date
620
+ ["0008,0023", "20000101", false], # Image Date
621
+ ["0008,0030", "000000.00", false], # Study Time
622
+ ["0008,0033", "000000.00", false], # Image Time
623
+ ["0008,0050", "", true], # Accession Number
624
+ ["0008,0080", "Institution", true], # Institution name
625
+ ["0008,0090", "Physician", true], # Referring Physician's name
626
+ ["0008,1010", "Station", true], # Station name
627
+ ["0008,1070", "Operator", true], # Operator's Name
628
+ ["0010,0010", "Patient", true], # Patient's name
629
+ ["0010,0020", "ID", true], # Patient's ID
630
+ ["0010,0030", "20000101", false], # Patient's Birth Date
631
+ ["0010,0040", "N", false], # Patient's Sex
632
+ ["0020,4000", "", false], # Image Comments
660
633
  ].transpose
661
634
  @tags = data[0]
662
635
  @values = data[1]
663
636
  @enumerations = data[2]
664
-
665
- # Tags to be deleted completely during anonymization
637
+ # Tags to be deleted completely during anonymization:
666
638
  @delete_tags = [
667
639
  ]
668
640
  end
@@ -670,6 +642,9 @@ module DICOM
670
642
  # Writes an identity file, which allows reidentification of DICOM files that have been anonymized
671
643
  # using the enumeration feature. Values are saved in a text file, using semi colon delineation.
672
644
  #
645
+ # @deprecated The identity file feature is deprecated!
646
+ # Please use the AuditTrail feature instead.
647
+ #
673
648
  def write_identity_file
674
649
  raise ArgumentError, "Expected String, got #{@identity_file.class}. Unable to write identity file." unless @identity_file.is_a?(String)
675
650
  # Open file and prepare to write text: