gdcm 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|