gdcm 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE +18 -0
- data/README.md +170 -0
- data/Rakefile +4 -0
- data/lib/gdcm/configuration.rb +75 -0
- data/lib/gdcm/package/info.rb +43 -0
- data/lib/gdcm/package.rb +174 -0
- data/lib/gdcm/shell.rb +78 -0
- data/lib/gdcm/tool/convert.rb +9 -0
- data/lib/gdcm/tool/identify.rb +9 -0
- data/lib/gdcm/tool.rb +217 -0
- data/lib/gdcm/utilities.rb +35 -0
- data/lib/gdcm/version.rb +17 -0
- data/lib/gdcm.rb +22 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 854120bce512df7e3a80fb22685e825f45f06a92598bbafe5d99aa19faca89ad
|
4
|
+
data.tar.gz: 916dce2f8f78d51513972d186aa392fd192b0d8f7a995ca77bb57d6d2e4dee94
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6514e8369d16786dddb8056f16e5ec6d14aacdf8ae7c54f4027006feace229d1bf9ba2eba3726f46be1ab70bc548c09daaa7e587face7404b4cc4bbc7076ada7
|
7
|
+
data.tar.gz: e343afd083dde1ad00d03e1d082694293293b7ffac4aef77e9c2ff5a33d82fe425448b7e107ff41bcc1e3c44c6ae50ef15bfdd50b3aec3c189c8f04411d7875f
|
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
2
|
+
a copy of this software and associated documentation files (the
|
3
|
+
"Software"), to deal in the Software without restriction, including
|
4
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
5
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
6
|
+
permit persons to whom the Software is furnished to do so, subject to
|
7
|
+
the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be
|
10
|
+
included in all copies or substantial portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
13
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
14
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
15
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
16
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
17
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
18
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
# GDCM
|
2
|
+
|
3
|
+
A ruby wrapper for [GDCM tools](http://gdcm.sourceforge.net/)
|
4
|
+
|
5
|
+
## Information
|
6
|
+
|
7
|
+
Inspired by MiniMagick ruby gem, this realization was created based on same DSL structure (but for GDCM tools).
|
8
|
+
|
9
|
+
## Requirements
|
10
|
+
|
11
|
+
GDCM command-line tool has to be installed. You can
|
12
|
+
check if you have it installed by running
|
13
|
+
|
14
|
+
```sh
|
15
|
+
$ gdcminfo --version
|
16
|
+
gdcminfo: gdcm 3.0.10
|
17
|
+
```
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add the gem to your Gemfile:
|
22
|
+
|
23
|
+
```rb
|
24
|
+
gem "gdcm"
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Let's first see a basic example.
|
30
|
+
|
31
|
+
```rb
|
32
|
+
require "gdcm"
|
33
|
+
|
34
|
+
package = GDCM::Package.open("original.dcm")
|
35
|
+
|
36
|
+
package.convert do |convert|
|
37
|
+
convert.raw
|
38
|
+
convert.verbose
|
39
|
+
end
|
40
|
+
|
41
|
+
package.path
|
42
|
+
package.write "output.dcm"
|
43
|
+
```
|
44
|
+
|
45
|
+
`GDCM::Package.open` makes a copy of the package, and further methods modify
|
46
|
+
that copy (the original stays untouched). The writing part is necessary because
|
47
|
+
the copy is just temporary, it gets garbage collected when we lose reference
|
48
|
+
to the package.
|
49
|
+
|
50
|
+
|
51
|
+
On the other hand, if we want the original package to actually *get* modified,
|
52
|
+
we can use `GDCM::Package.new`.
|
53
|
+
|
54
|
+
```rb
|
55
|
+
package = GDCM::Package.new("original.dcm")
|
56
|
+
package.path
|
57
|
+
|
58
|
+
package.convert do |convert|
|
59
|
+
convert.raw
|
60
|
+
convert.verbose
|
61
|
+
end
|
62
|
+
# Not calling #write, because it's not a copy
|
63
|
+
```
|
64
|
+
|
65
|
+
### Attributes
|
66
|
+
|
67
|
+
|
68
|
+
To get the all information about the package, GDCM gives you a handy method
|
69
|
+
which returns the output from `gdcminfo` in hash format:
|
70
|
+
|
71
|
+
```rb
|
72
|
+
package.info.data #=>
|
73
|
+
#{"MediaStorage"=>"1.2.840.10008.5.1.4.1.1.77.1.5.1",
|
74
|
+
# "TransferSyntax"=>"1.2.840.10008.1.2.4.70",
|
75
|
+
# "NumberOfDimensions"=>"2",
|
76
|
+
# "Dimensions"=>"(4000,4000,1)",
|
77
|
+
# "SamplesPerPixel"=>"3",
|
78
|
+
# "BitsAllocated"=>"8",
|
79
|
+
# "BitsStored"=>"8",
|
80
|
+
# "HighBit"=>"7",
|
81
|
+
# "PixelRepresentation"=>"0",
|
82
|
+
# "ScalarType found"=>"UINT8",
|
83
|
+
# "PhotometricInterpretation"=>"RGB",
|
84
|
+
# "PlanarConfiguration"=>"0",
|
85
|
+
# "Origin"=>"(0,0,0)",
|
86
|
+
# "Spacing"=>"(1,1,1)",
|
87
|
+
# "DirectionCosines"=>"(1,0,0,0,1,0)",
|
88
|
+
# "Rescale Intercept/Slope"=>"(0,1)",
|
89
|
+
# "Orientation Label"=>"AXIAL"}
|
90
|
+
```
|
91
|
+
|
92
|
+
|
93
|
+
### Configuration
|
94
|
+
|
95
|
+
```rb
|
96
|
+
GDCM.configure do |config|
|
97
|
+
config.timeout = 5
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
### Package validation
|
102
|
+
|
103
|
+
By default, GDCM validates package each time it's opening them. It
|
104
|
+
validates them by running `gdcminfo` on them, and see if GDCM tools finds
|
105
|
+
them valid. This adds slight overhead to the whole processing. Sometimes it's
|
106
|
+
safe to assume that all input and output packages are valid by default and turn
|
107
|
+
off validation:
|
108
|
+
|
109
|
+
```rb
|
110
|
+
GDCM.configure do |config|
|
111
|
+
config.validate_on_create = false
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
You can test whether an package is valid:
|
116
|
+
|
117
|
+
```rb
|
118
|
+
package.valid?
|
119
|
+
package.validate! # raises GDCM::Invalid if package is invalid
|
120
|
+
```
|
121
|
+
|
122
|
+
### Logging
|
123
|
+
|
124
|
+
You can choose to log GDCM commands and their execution times:
|
125
|
+
|
126
|
+
```rb
|
127
|
+
GDCM.logger.level = Logger::DEBUG
|
128
|
+
```
|
129
|
+
```
|
130
|
+
D, [2022-04-11T12:07:39.240238 #59063] DEBUG -- : [0.11s] gdcminfo /var/folders/4d/k113_9r544nfj8k0bfxtjx0m0000gn/T/gdcm20220411-59063-8yvk5s.dcm
|
131
|
+
```
|
132
|
+
|
133
|
+
In Rails you'll probably want to set `GDCM.logger = Rails.logger`.
|
134
|
+
|
135
|
+
### Metal
|
136
|
+
|
137
|
+
If you want to be close to the metal, you can use GDCM's command-line
|
138
|
+
tools directly.
|
139
|
+
|
140
|
+
```rb
|
141
|
+
GDCM::Tool::Convert.new do |convert|
|
142
|
+
convert.raw
|
143
|
+
convert.verbose
|
144
|
+
convert << "input.dcm"
|
145
|
+
convert << "output.dcm"
|
146
|
+
end #=> `gdcmconv --raw --verbose input.dcm output.dcm`
|
147
|
+
|
148
|
+
# OR
|
149
|
+
|
150
|
+
convert = GDCM::Tool::Convert.new
|
151
|
+
convert.raw
|
152
|
+
convert.verbose
|
153
|
+
convert << "input.jpg"
|
154
|
+
convert << "output.jpg"
|
155
|
+
convert.call #=> `gdcmconv --raw --verbose input.dcm output.dcm`
|
156
|
+
```
|
157
|
+
|
158
|
+
## Troubleshooting
|
159
|
+
|
160
|
+
### Errors being raised when they shouldn't
|
161
|
+
|
162
|
+
|
163
|
+
If you're using the tool directly, you can pass `whiny: false` value to the
|
164
|
+
constructor:
|
165
|
+
|
166
|
+
```rb
|
167
|
+
GDCM::Tool::Identify.new(whiny: false) do |b|
|
168
|
+
b.help
|
169
|
+
end
|
170
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'gdcm/utilities'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module GDCM
|
5
|
+
module Configuration
|
6
|
+
##
|
7
|
+
# If you don't want commands to take too long, you can set a timeout (in
|
8
|
+
# seconds).
|
9
|
+
#
|
10
|
+
# @return [Integer]
|
11
|
+
#
|
12
|
+
attr_accessor :timeout
|
13
|
+
##
|
14
|
+
# When get to `true`, it outputs each command to STDOUT in their shell
|
15
|
+
# version.
|
16
|
+
#
|
17
|
+
# @return [Boolean]
|
18
|
+
#
|
19
|
+
attr_reader :debug
|
20
|
+
##
|
21
|
+
# Logger for {#debug}, default is `GDCM::Logger.new(STDOUT)`, but
|
22
|
+
# you can override it, for example if you want the logs to be written to
|
23
|
+
# a file.
|
24
|
+
#
|
25
|
+
# @return [Logger]
|
26
|
+
#
|
27
|
+
attr_accessor :logger
|
28
|
+
|
29
|
+
##
|
30
|
+
# If set to `true`, it will `identify` every newly created image, and raise
|
31
|
+
# `MiniMagick::Invalid` if the image is not valid. Useful for validating
|
32
|
+
# user input, although it adds a bit of overhead. Defaults to `true`.
|
33
|
+
#
|
34
|
+
# @return [Boolean]
|
35
|
+
#
|
36
|
+
attr_accessor :validate_on_create
|
37
|
+
##
|
38
|
+
# If set to `true`, it will `identify` every image that gets written (with
|
39
|
+
# {GDCM::Image#write}), and raise `GDCM::Invalid` if the image
|
40
|
+
# is not valid. Useful for validating that processing was sucessful,
|
41
|
+
# although it adds a bit of overhead. Defaults to `true`.
|
42
|
+
#
|
43
|
+
# @return [Boolean]
|
44
|
+
|
45
|
+
#
|
46
|
+
attr_accessor :whiny
|
47
|
+
|
48
|
+
##
|
49
|
+
# Instructs GDCM how to execute the shell commands. Available
|
50
|
+
# APIs are "open3" (default) and "posix-spawn" (requires the "posix-spawn"
|
51
|
+
# gem).
|
52
|
+
#
|
53
|
+
# @return [String]
|
54
|
+
#
|
55
|
+
attr_accessor :shell_api
|
56
|
+
|
57
|
+
def self.extended(base)
|
58
|
+
base.validate_on_create = true
|
59
|
+
base.whiny = true
|
60
|
+
base.shell_api = "open3"
|
61
|
+
base.logger = Logger.new($stdout).tap { |l| l.level = Logger::INFO }
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# @yield [self]
|
66
|
+
# @example
|
67
|
+
# GDCM.configure do |config|
|
68
|
+
# config.timeout = 5
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
def configure
|
72
|
+
yield self
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module GDCM
|
2
|
+
class Package
|
3
|
+
class Info
|
4
|
+
attr_reader :package_instance
|
5
|
+
|
6
|
+
def initialize package_instance
|
7
|
+
@package_instance = package_instance
|
8
|
+
end
|
9
|
+
|
10
|
+
def data
|
11
|
+
if meta.respond_to?(:lines)
|
12
|
+
meta.lines.each_with_object({}) do |line, memo|
|
13
|
+
case line
|
14
|
+
when /^MediaStorage is (?<media_storage>[\d.]+)/
|
15
|
+
memo['MediaStorage'] = $~[:media_storage]
|
16
|
+
when /^TransferSyntax is (?<transfer_syntax>[\d.]+)/
|
17
|
+
memo['TransferSyntax'] = $~[:transfer_syntax]
|
18
|
+
else
|
19
|
+
key, _, value = line.partition(/:[\s]*/).map(&:strip)
|
20
|
+
|
21
|
+
memo[key] = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def meta
|
28
|
+
@meta ||= identify
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear
|
32
|
+
@meta = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def identify
|
36
|
+
GDCM::Tool::Identify.new do |builder|
|
37
|
+
yield builder if block_given?
|
38
|
+
builder << package_instance.path
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/gdcm/package.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'stringio'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
require 'gdcm/package/info'
|
6
|
+
require 'gdcm/utilities'
|
7
|
+
|
8
|
+
module GDCM
|
9
|
+
class Package
|
10
|
+
|
11
|
+
##
|
12
|
+
# This is the primary loading method used by all of the other class
|
13
|
+
# methods.
|
14
|
+
#
|
15
|
+
# Use this to pass in a stream object. Must respond to #read(size) or be a
|
16
|
+
# binary string object (BLOB)
|
17
|
+
#
|
18
|
+
# @param stream [#read, String] Some kind of stream object that needs
|
19
|
+
# to be read or is a binary String blob
|
20
|
+
# @param ext [String] A manual extension to use for reading the file. Not
|
21
|
+
# required, but if you are having issues, give this a try.
|
22
|
+
# @return [GDCM::Image]
|
23
|
+
#
|
24
|
+
def self.read(stream, ext = nil)
|
25
|
+
if stream.is_a?(String)
|
26
|
+
stream = StringIO.new(stream)
|
27
|
+
end
|
28
|
+
|
29
|
+
create(ext) { |file| IO.copy_stream(stream, file) }
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Opens a specific file either on the local file system.
|
34
|
+
# Use this if you don't want to overwrite file.
|
35
|
+
#
|
36
|
+
# Extension is either guessed from the path or you can specify it as a
|
37
|
+
# second parameter.
|
38
|
+
#
|
39
|
+
# @param path [String] Either a local file path
|
40
|
+
# @param ext [String] Specify the extension you want to read it as
|
41
|
+
# @param options [Hash] Specify options for the open method
|
42
|
+
# @return [GDCM::Image] The loaded file
|
43
|
+
#
|
44
|
+
def self.open(path, ext = nil, options = {})
|
45
|
+
options, ext = ext, nil if ext.is_a?(Hash)
|
46
|
+
|
47
|
+
# Don't use Kernel#open, but reuse its logic
|
48
|
+
openable =
|
49
|
+
if path.respond_to?(:open)
|
50
|
+
path
|
51
|
+
else
|
52
|
+
options = { binmode: true }.merge(options)
|
53
|
+
Pathname(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
ext ||= File.extname(openable.to_s)
|
57
|
+
ext.sub!(/:.*/, '') # hack for filenames that include a colon
|
58
|
+
|
59
|
+
openable.open(**options) { |file| read(file, ext) }
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Used to create a new file object data-copy.
|
64
|
+
#
|
65
|
+
# Takes an extension in a block and can be used to build a new file
|
66
|
+
# object. Used by both {.open} and {.read} to create a new object. Ensures
|
67
|
+
# we have a good tempfile.
|
68
|
+
#
|
69
|
+
# @param ext [String] Specify the extension you want to read it as
|
70
|
+
# @param validate [Boolean] If false, skips validation of the created
|
71
|
+
# file. Defaults to true.
|
72
|
+
# @yield [Tempfile] You can #write bits to this object to create the new
|
73
|
+
# file
|
74
|
+
# @return [GDCM::Image] The created file
|
75
|
+
#
|
76
|
+
def self.create(ext = nil, validate = GDCM.validate_on_create, &block)
|
77
|
+
tempfile = GDCM::Utilities.tempfile(ext.to_s.downcase, &block)
|
78
|
+
|
79
|
+
new(tempfile.path, tempfile).tap do |file|
|
80
|
+
file.validate! if validate
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_reader :path
|
85
|
+
attr_reader :tempfile
|
86
|
+
attr_reader :meta
|
87
|
+
|
88
|
+
def initialize(input_path, tempfile = nil)
|
89
|
+
@path = input_path.to_s
|
90
|
+
@tempfile = tempfile
|
91
|
+
end
|
92
|
+
|
93
|
+
def info
|
94
|
+
@info ||= GDCM::Package::Info.new(self)
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_blob
|
98
|
+
File.binread(path)
|
99
|
+
end
|
100
|
+
|
101
|
+
def valid?
|
102
|
+
validate!
|
103
|
+
true
|
104
|
+
rescue GDCM::Invalid
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate!
|
109
|
+
identify
|
110
|
+
rescue GDCM::Error => error
|
111
|
+
raise GDCM::Invalid, error.message
|
112
|
+
end
|
113
|
+
|
114
|
+
def identify
|
115
|
+
info.identify
|
116
|
+
end
|
117
|
+
|
118
|
+
def convert
|
119
|
+
if @tempfile
|
120
|
+
new_tempfile = GDCM::Utilities.tempfile(".dcm")
|
121
|
+
new_path = new_tempfile.path
|
122
|
+
else
|
123
|
+
new_path = Pathname(path).sub_ext(".dcm").to_s
|
124
|
+
end
|
125
|
+
|
126
|
+
input_path = path.dup
|
127
|
+
|
128
|
+
GDCM::Tool::Convert.new do |convert|
|
129
|
+
yield convert if block_given?
|
130
|
+
convert << input_path
|
131
|
+
convert << new_path
|
132
|
+
end
|
133
|
+
|
134
|
+
if @tempfile
|
135
|
+
destroy!
|
136
|
+
@tempfile = new_tempfile
|
137
|
+
else
|
138
|
+
File.delete(path) unless path == new_path
|
139
|
+
end
|
140
|
+
|
141
|
+
path.replace new_path
|
142
|
+
info.clear
|
143
|
+
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Writes the temporary file out to either a file location (by passing in a
|
149
|
+
# String) or by passing in a Stream that you can #write(chunk) to
|
150
|
+
# repeatedly
|
151
|
+
#
|
152
|
+
# @param output_to [String, Pathname, #read] Some kind of stream object
|
153
|
+
# that needs to be read or a file path as a String
|
154
|
+
#
|
155
|
+
def write(output_to)
|
156
|
+
case output_to
|
157
|
+
when String, Pathname
|
158
|
+
FileUtils.copy_file path, output_to unless path == output_to.to_s
|
159
|
+
else
|
160
|
+
IO.copy_stream File.open(path, "rb"), output_to
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Destroys the tempfile (created by {.open}) if it exists.
|
166
|
+
#
|
167
|
+
def destroy!
|
168
|
+
if @tempfile
|
169
|
+
FileUtils.rm_f @tempfile.path.sub(/mpc$/, "cache") if @tempfile.path.end_with?(".mpc")
|
170
|
+
@tempfile.unlink
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/gdcm/shell.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require "timeout"
|
2
|
+
require "benchmark"
|
3
|
+
|
4
|
+
module GDCM
|
5
|
+
##
|
6
|
+
# Sends commands to the shell (more precisely, it sends commands directly to
|
7
|
+
# the operating system).
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
#
|
11
|
+
class Shell
|
12
|
+
|
13
|
+
def run(command, options = {})
|
14
|
+
stdout, stderr, status = execute(command, stdin: options[:stdin])
|
15
|
+
|
16
|
+
if status != 0 && options.fetch(:whiny, GDCM.whiny)
|
17
|
+
fail GDCM::Error, "`#{command.join(" ")}` failed with error:\n#{stderr}"
|
18
|
+
end
|
19
|
+
|
20
|
+
$stderr.print(stderr) unless options[:stderr] == false
|
21
|
+
|
22
|
+
[stdout, stderr, status]
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute(command, options = {})
|
26
|
+
stdout, stderr, status =
|
27
|
+
log(command.join(" ")) do
|
28
|
+
send("execute_#{GDCM.shell_api.gsub("-", "_")}", command, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
[stdout, stderr, status.exitstatus]
|
32
|
+
rescue Errno::ENOENT, IOError
|
33
|
+
["", "executable not found: \"#{command.first}\"", 127]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def execute_open3(command, options = {})
|
39
|
+
require "open3"
|
40
|
+
|
41
|
+
# We would ideally use Open3.capture3, but it wouldn't allow us to
|
42
|
+
# terminate the command after timing out.
|
43
|
+
Open3.popen3(*command) do |in_w, out_r, err_r, thread|
|
44
|
+
[in_w, out_r, err_r].each(&:binmode)
|
45
|
+
stdout_reader = Thread.new { out_r.read }
|
46
|
+
stderr_reader = Thread.new { err_r.read }
|
47
|
+
begin
|
48
|
+
in_w.write options[:stdin].to_s
|
49
|
+
rescue Errno::EPIPE
|
50
|
+
end
|
51
|
+
in_w.close
|
52
|
+
|
53
|
+
unless thread.join(GDCM.timeout)
|
54
|
+
Process.kill("TERM", thread.pid) rescue nil
|
55
|
+
Process.waitpid(thread.pid) rescue nil
|
56
|
+
raise Timeout::Error, "GDCM command timed out: #{command}"
|
57
|
+
end
|
58
|
+
|
59
|
+
[stdout_reader.value, stderr_reader.value, thread.value]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def execute_posix_spawn(command, options = {})
|
64
|
+
require "posix-spawn"
|
65
|
+
child = POSIX::Spawn::Child.new(*command, input: options[:stdin].to_s, timeout: GDCM.timeout)
|
66
|
+
[child.out, child.err, child.status]
|
67
|
+
rescue POSIX::Spawn::TimeoutExceeded
|
68
|
+
raise Timeout::Error, "GDCM command timed out: #{command}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def log(command, &block)
|
72
|
+
value = nil
|
73
|
+
duration = Benchmark.realtime { value = block.call }
|
74
|
+
GDCM.logger.debug "[%.2fs] %s" % [duration, command]
|
75
|
+
value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/gdcm/tool.rb
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
require "gdcm/shell"
|
2
|
+
|
3
|
+
module GDCM
|
4
|
+
##
|
5
|
+
# Abstract class that wraps command-line tools. It shouldn't be used directly,
|
6
|
+
# but through one of its subclasses. Use
|
7
|
+
# this class if you want to be closer to the metal and execute GDCM
|
8
|
+
# commands directly, but still with a nice Ruby interface.
|
9
|
+
#
|
10
|
+
class Tool
|
11
|
+
##
|
12
|
+
# Aside from classic instantiation, it also accepts a block, and then
|
13
|
+
# executes the command in the end.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# version = GDCM::Tool::Identify.new { |b| b.version }
|
17
|
+
# puts version
|
18
|
+
#
|
19
|
+
# @return [GDCM::Tool, String] If no block is given, returns an
|
20
|
+
# instance of the tool, if block is given, returns the output of the
|
21
|
+
# command.
|
22
|
+
#
|
23
|
+
def self.new(*args)
|
24
|
+
instance = super(*args)
|
25
|
+
|
26
|
+
if block_given?
|
27
|
+
yield instance
|
28
|
+
instance.call
|
29
|
+
else
|
30
|
+
instance
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @private
|
35
|
+
attr_reader :name, :args
|
36
|
+
|
37
|
+
# @param name [String]
|
38
|
+
# @param options [Hash]
|
39
|
+
# @option options [Boolean] :whiny Whether to raise errors on non-zero
|
40
|
+
# exit codes.
|
41
|
+
# @example
|
42
|
+
# GDCM::Tool::Identify.new(whiny: false) do |identify|
|
43
|
+
# identify.help # returns exit status 1, which would otherwise throw an error
|
44
|
+
# end
|
45
|
+
def initialize(name, options = {})
|
46
|
+
@name = name
|
47
|
+
@args = []
|
48
|
+
@whiny = options.is_a?(Hash) ? options.fetch(:whiny, GDCM.whiny) : options
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Executes the command that has been built up.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# convert = GDCM::Tool::Convert.new
|
56
|
+
# convert.resize("500x500")
|
57
|
+
# convert << "path/to/file.dcm"
|
58
|
+
# convert.call # executes `convert --resize 500x500 path/to/file.dcm`
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# convert = GDCM::Tool::Convert.new
|
62
|
+
# # build the command
|
63
|
+
# convert.call do |stdout, stderr, status|
|
64
|
+
# # ...
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# @yield [Array] Optionally yields stdout, stderr, and exit status
|
68
|
+
#
|
69
|
+
# @return [String] Returns the output of the command
|
70
|
+
#
|
71
|
+
def call(*args)
|
72
|
+
options = args[-1].is_a?(Hash) ? args.pop : {}
|
73
|
+
whiny = args.fetch(0, @whiny)
|
74
|
+
|
75
|
+
options[:whiny] = whiny
|
76
|
+
options[:stderr] = false if block_given?
|
77
|
+
|
78
|
+
shell = GDCM::Shell.new
|
79
|
+
stdout, stderr, status = shell.run(command, options)
|
80
|
+
yield stdout, stderr, status if block_given?
|
81
|
+
|
82
|
+
stdout.chomp("\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# The currently built-up command.
|
87
|
+
#
|
88
|
+
# @return [Array<String>]
|
89
|
+
#
|
90
|
+
# @example
|
91
|
+
# convert = GDCM::Tool::Convert.new
|
92
|
+
# convert.resize "500x500"
|
93
|
+
# convert.contrast
|
94
|
+
# convert.command #=> ["convert", "--resize", "500x500", "--contrast"]
|
95
|
+
#
|
96
|
+
def command
|
97
|
+
[*executable, *args]
|
98
|
+
end
|
99
|
+
|
100
|
+
def executable
|
101
|
+
exe = [name]
|
102
|
+
exe
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Appends raw options, useful for appending file paths.
|
107
|
+
#
|
108
|
+
# @return [self]
|
109
|
+
#
|
110
|
+
def <<(arg)
|
111
|
+
args << arg.to_s
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Merges a list of raw options.
|
117
|
+
#
|
118
|
+
# @return [self]
|
119
|
+
#
|
120
|
+
def merge!(new_args)
|
121
|
+
new_args.each { |arg| self << arg }
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Changes the last operator to its "plus" form.
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# GDCM::Tool::Convert.new do |convert|
|
130
|
+
# convert.antialias.+
|
131
|
+
# convert.distort.+("Perspective", "0,0,4,5 89,0,45,46")
|
132
|
+
# end
|
133
|
+
# # executes `convert +antialias +distort Perspective '0,0,4,5 89,0,45,46'`
|
134
|
+
#
|
135
|
+
# @return [self]
|
136
|
+
#
|
137
|
+
def +(*values)
|
138
|
+
args[-1] = args[-1].sub(/^-/, '+')
|
139
|
+
self.merge!(values)
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Create an GDCM stack in the command (surround.
|
145
|
+
#
|
146
|
+
# @example
|
147
|
+
# GDCM::Tool::Convert.new do |convert|
|
148
|
+
# convert << "1.dcm"
|
149
|
+
# convert.stack do |stack|
|
150
|
+
# stack << "2.dcm"
|
151
|
+
# stack.rotate(30)
|
152
|
+
# end
|
153
|
+
# convert.append.+
|
154
|
+
# convert << "3.dcm"
|
155
|
+
# end
|
156
|
+
# # executes `convert 1.dcm \( 2.dcm --rotate 30 \) +append 3.dcm`
|
157
|
+
#
|
158
|
+
def stack(*args)
|
159
|
+
self << "("
|
160
|
+
args.each do |value|
|
161
|
+
case value
|
162
|
+
when Hash then value.each { |key, value| send(key, *value) }
|
163
|
+
when String then self << value
|
164
|
+
end
|
165
|
+
end
|
166
|
+
yield self if block_given?
|
167
|
+
self << ")"
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Adds GDCM's pseudo-filename `-` for standard input.
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# identify = GDCM::Tool::Identify.new
|
175
|
+
# identify.stdin
|
176
|
+
# identify.call(stdin: content)
|
177
|
+
# # executes `identify -` with the given standard input
|
178
|
+
#
|
179
|
+
def stdin
|
180
|
+
self << "-"
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Adds GDCM's pseudo-filename `-` for standard output.
|
185
|
+
#
|
186
|
+
# @example
|
187
|
+
# content = GDCM::Tool::Convert.new do |convert|
|
188
|
+
# convert << "1.dcm"
|
189
|
+
# convert.auto_orient
|
190
|
+
# convert.stdout
|
191
|
+
# end
|
192
|
+
# # executes `convert 1.dcm --auto-orient -` which returns file contents
|
193
|
+
#
|
194
|
+
def stdout
|
195
|
+
self << "-"
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# Any undefined method will be transformed into a CLI option
|
200
|
+
#
|
201
|
+
# @example
|
202
|
+
# convert = GDCM::Tool.new("convert")
|
203
|
+
# convert.adaptive_blur("...")
|
204
|
+
# convert.foo_bar
|
205
|
+
# convert.command.join(" ") # => "convert --adaptive-blur ... --foo-bar"
|
206
|
+
#
|
207
|
+
def method_missing(name, *args)
|
208
|
+
option = "--#{name.to_s.tr('_', '-')}"
|
209
|
+
self << option
|
210
|
+
self.merge!(args)
|
211
|
+
self
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
require "gdcm/tool/convert"
|
217
|
+
require "gdcm/tool/identify"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "tempfile"
|
2
|
+
|
3
|
+
module GDCM
|
4
|
+
# @private
|
5
|
+
module Utilities
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
##
|
10
|
+
# Cross-platform way of finding an executable in the $PATH.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# GDCM::Utilities.which('ruby') #=> "/usr/bin/ruby"
|
14
|
+
#
|
15
|
+
def which(cmd)
|
16
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
17
|
+
ENV.fetch('PATH').split(File::PATH_SEPARATOR).each do |path|
|
18
|
+
exts.each do |ext|
|
19
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
20
|
+
return exe if File.executable? exe
|
21
|
+
end
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def tempfile(extension)
|
27
|
+
Tempfile.new(["gdcm", extension]).tap do |tempfile|
|
28
|
+
tempfile.binmode
|
29
|
+
yield tempfile if block_given?
|
30
|
+
tempfile.close
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/gdcm/version.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module GDCM
|
2
|
+
##
|
3
|
+
# @return [Gem::Version]
|
4
|
+
#
|
5
|
+
def self.version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 1
|
11
|
+
MINOR = 0
|
12
|
+
TINY = 0
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
16
|
+
end
|
17
|
+
end
|
data/lib/gdcm.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'gdcm/version'
|
2
|
+
require 'gdcm/configuration'
|
3
|
+
|
4
|
+
module GDCM
|
5
|
+
extend GDCM::Configuration
|
6
|
+
|
7
|
+
##
|
8
|
+
# Returns GDCM's version.
|
9
|
+
#
|
10
|
+
# @return [String]
|
11
|
+
def self.cli_version
|
12
|
+
output = GDCM::Tool::Identify.new(&:version)
|
13
|
+
output[/\d+\.\d+\.\d+(-\d+)?/]
|
14
|
+
end
|
15
|
+
|
16
|
+
class Error < RuntimeError; end
|
17
|
+
class Invalid < StandardError; end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'gdcm/tool'
|
22
|
+
require 'gdcm/package'
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gdcm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- sanzstez
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-04-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: posix-spawn
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: webmock
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Ruby adapter for GDCM tools for DICOM medical files.
|
56
|
+
email:
|
57
|
+
- sanzstez@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- LICENSE
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- lib/gdcm.rb
|
66
|
+
- lib/gdcm/configuration.rb
|
67
|
+
- lib/gdcm/package.rb
|
68
|
+
- lib/gdcm/package/info.rb
|
69
|
+
- lib/gdcm/shell.rb
|
70
|
+
- lib/gdcm/tool.rb
|
71
|
+
- lib/gdcm/tool/convert.rb
|
72
|
+
- lib/gdcm/tool/identify.rb
|
73
|
+
- lib/gdcm/utilities.rb
|
74
|
+
- lib/gdcm/version.rb
|
75
|
+
homepage: https://github.com/sanzstez/gdcm
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '2.0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements:
|
94
|
+
- You must have GDCM tools installed
|
95
|
+
rubygems_version: 3.0.9
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Ruby adapter for GDCM tools for DICOM medical files.
|
99
|
+
test_files: []
|