dcmtk 1.0.1
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 +177 -0
- data/Rakefile +4 -0
- data/lib/dcmtk/configuration.rb +64 -0
- data/lib/dcmtk/package.rb +157 -0
- data/lib/dcmtk/shell.rb +78 -0
- data/lib/dcmtk/tool/convert.rb +30 -0
- data/lib/dcmtk/tool.rb +144 -0
- data/lib/dcmtk/utilities.rb +35 -0
- data/lib/dcmtk/version.rb +17 -0
- data/lib/dcmtk.rb +34 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 02e561669b448629495a372ce64dcc1fb60f2b79f2d8d3be5ec0994c8f398378
|
|
4
|
+
data.tar.gz: d0883775324f3e65da156289a5a839cbd19e511ada8a3c0ac0b7dbd49f1d1757
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 585a11d3adecc9ee835cd4b5b473ad2553d3015d99054c7cc933fb1db1c32eee7fea880abb4d100164d00f2d8c61831b30edb41d2584f2994df6bb25f17e061c
|
|
7
|
+
data.tar.gz: 73cb9ff4b1750a1fb460ce2e0be672f5891c22e56c8c90c2ecc5a99d3d642185d8c87e921ba59d40af5e1a2ec09b117e66cc00813d4f0e7549e4e429cfff4fe5
|
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,177 @@
|
|
|
1
|
+
# DCMTK
|
|
2
|
+
|
|
3
|
+
A Ruby wrapper for [DCMTK tools](https://dicom.offis.de/dcmtk.php.en) - specifically `dcmj2pnm` or `dcm2img` for converting DICOM medical files to standard image formats.
|
|
4
|
+
|
|
5
|
+
## Information
|
|
6
|
+
|
|
7
|
+
Inspired by MiniMagick Ruby gem, this gem provides a DSL for working with DCMTK command-line tools.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
DCMTK command-line tools must be installed. You can check if you have it installed by running:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
$ dcmj2pnm --version
|
|
15
|
+
$dcmtk: dcmj2pnm v3.6.7 2022-04-22
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
19
|
+
|
|
20
|
+
On macOS:
|
|
21
|
+
```sh
|
|
22
|
+
brew install dcmtk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
On Ubuntu/Debian:
|
|
26
|
+
```sh
|
|
27
|
+
apt-get install dcmtk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Gem Installation
|
|
31
|
+
|
|
32
|
+
Add the gem to your Gemfile:
|
|
33
|
+
|
|
34
|
+
```rb
|
|
35
|
+
gem "dcmtk"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Let's first see a basic example - converting a DICOM file to PNG:
|
|
41
|
+
|
|
42
|
+
```rb
|
|
43
|
+
require "dcmtk"
|
|
44
|
+
|
|
45
|
+
package = DCMTK::Package.open("scan.dcm")
|
|
46
|
+
package.convert(format: :png)
|
|
47
|
+
package.write("output.png")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`DCMTK::Package.open` makes a copy of the package, and further methods modify that copy (the original stays untouched). The writing part is necessary because the copy is just temporary, it gets garbage collected when we lose reference to the package.
|
|
51
|
+
|
|
52
|
+
On the other hand, if we want the original package to actually *get* modified, we can use `DCMTK::Package.new`:
|
|
53
|
+
|
|
54
|
+
```rb
|
|
55
|
+
package = DCMTK::Package.new("scan.dcm")
|
|
56
|
+
package.convert(format: :png)
|
|
57
|
+
# Output file is created at scan.png (same name, different extension)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Supported Formats
|
|
61
|
+
|
|
62
|
+
The gem supports converting DICOM files to these formats:
|
|
63
|
+
|
|
64
|
+
| Format | Symbol | dcmj2pnm Flag |
|
|
65
|
+
|--------|--------|---------------|
|
|
66
|
+
| PNG | `:png` | `+on` |
|
|
67
|
+
| BMP | `:bmp` | `+ob` |
|
|
68
|
+
| JPEG | `:jpeg`| `+oj` |
|
|
69
|
+
|
|
70
|
+
```rb
|
|
71
|
+
# Convert to different formats
|
|
72
|
+
package.convert(format: :png) # PNG output
|
|
73
|
+
package.convert(format: :bmp) # BMP output
|
|
74
|
+
package.convert(format: :jpeg) # JPEG output
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Configuration
|
|
78
|
+
|
|
79
|
+
```rb
|
|
80
|
+
DCMTK.configure do |config|
|
|
81
|
+
config.timeout = 5
|
|
82
|
+
config.whiny = true # raise errors on non-zero exit codes
|
|
83
|
+
config.cli = "dcmj2pnm" # force specific CLI (see below)
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### CLI Tool Selection
|
|
88
|
+
|
|
89
|
+
By default, the gem auto-detects which CLI tool to use:
|
|
90
|
+
- If `dcm2img` is found in PATH, it will be used
|
|
91
|
+
- Otherwise, falls back to `dcmj2pnm`
|
|
92
|
+
|
|
93
|
+
You can force a specific CLI tool:
|
|
94
|
+
|
|
95
|
+
```rb
|
|
96
|
+
# Force dcmj2pnm
|
|
97
|
+
DCMTK.configure do |config|
|
|
98
|
+
config.cli = "dcmj2pnm"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Force dcm2img
|
|
102
|
+
DCMTK.configure do |config|
|
|
103
|
+
config.cli = "dcm2img"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check which CLI is being used
|
|
107
|
+
DCMTK.convert_cli #=> "dcm2img" or "dcmj2pnm"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Logging
|
|
111
|
+
|
|
112
|
+
You can choose to log DCMTK commands and their execution times:
|
|
113
|
+
|
|
114
|
+
```rb
|
|
115
|
+
DCMTK.logger.level = Logger::DEBUG
|
|
116
|
+
```
|
|
117
|
+
```
|
|
118
|
+
D, [2024-01-15T12:07:39.240238 #59063] DEBUG -- : [0.11s] dcmj2pnm +on /tmp/dcmtk20240115-59063-8yvk5s.dcm /tmp/output.png
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
In Rails you'll probably want to set `DCMTK.logger = Rails.logger`.
|
|
122
|
+
|
|
123
|
+
### Metal
|
|
124
|
+
|
|
125
|
+
If you want to be close to the metal, you can use DCMTK's command-line tools directly:
|
|
126
|
+
|
|
127
|
+
```rb
|
|
128
|
+
DCMTK::Tool::Convert.new do |convert|
|
|
129
|
+
convert.format(:png)
|
|
130
|
+
convert << "input.dcm"
|
|
131
|
+
convert << "output.png"
|
|
132
|
+
end #=> `dcmj2pnm +on input.dcm output.png`
|
|
133
|
+
|
|
134
|
+
# OR
|
|
135
|
+
|
|
136
|
+
convert = DCMTK::Tool::Convert.new
|
|
137
|
+
convert.format(:jpeg)
|
|
138
|
+
convert << "input.dcm"
|
|
139
|
+
convert << "output.jpg"
|
|
140
|
+
convert.call #=> `dcmj2pnm +oj input.dcm output.jpg`
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
You can also pass additional flags using method_missing:
|
|
144
|
+
|
|
145
|
+
```rb
|
|
146
|
+
DCMTK::Tool::Convert.new do |convert|
|
|
147
|
+
convert.format(:png)
|
|
148
|
+
convert.verbose # adds --verbose flag
|
|
149
|
+
convert << "input.dcm"
|
|
150
|
+
convert << "output.png"
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Troubleshooting
|
|
155
|
+
|
|
156
|
+
### Errors being raised when they shouldn't
|
|
157
|
+
|
|
158
|
+
If you're using the tool directly, you can pass `whiny: false` value to the constructor:
|
|
159
|
+
|
|
160
|
+
```rb
|
|
161
|
+
DCMTK::Tool::Convert.new(whiny: false) do |convert|
|
|
162
|
+
convert.help
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Command not found
|
|
167
|
+
|
|
168
|
+
Make sure DCMTK tools are installed and either `dcm2img` or `dcmj2pnm` is in your PATH:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
which dcm2img
|
|
172
|
+
which dcmj2pnm
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require 'dcmtk/utilities'
|
|
2
|
+
require 'logger'
|
|
3
|
+
|
|
4
|
+
module DCMTK
|
|
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
|
+
# Logger for debug output, default is `Logger.new(STDOUT)`, but
|
|
15
|
+
# you can override it, for example if you want the logs to be written to
|
|
16
|
+
# a file.
|
|
17
|
+
#
|
|
18
|
+
# @return [Logger]
|
|
19
|
+
#
|
|
20
|
+
attr_accessor :logger
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# If set to `true`, it will raise errors on non-zero exit codes.
|
|
24
|
+
# Defaults to `true`.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
#
|
|
28
|
+
attr_accessor :whiny
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# Instructs DCMTK how to execute the shell commands. Available
|
|
32
|
+
# APIs are "open3" (default) and "posix-spawn" (requires the "posix-spawn"
|
|
33
|
+
# gem).
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
#
|
|
37
|
+
attr_accessor :shell_api
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Force a specific CLI command for conversion.
|
|
41
|
+
# When nil (default), auto-detects: dcm2img if available, otherwise dcmj2pnm.
|
|
42
|
+
#
|
|
43
|
+
# @return [String, nil]
|
|
44
|
+
#
|
|
45
|
+
attr_accessor :cli
|
|
46
|
+
|
|
47
|
+
def self.extended(base)
|
|
48
|
+
base.whiny = true
|
|
49
|
+
base.shell_api = "open3"
|
|
50
|
+
base.logger = Logger.new($stdout).tap { |l| l.level = Logger::INFO }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @yield [self]
|
|
55
|
+
# @example
|
|
56
|
+
# DCMTK.configure do |config|
|
|
57
|
+
# config.timeout = 5
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
def configure
|
|
61
|
+
yield self
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
require 'stringio'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
require 'dcmtk/utilities'
|
|
7
|
+
|
|
8
|
+
module DCMTK
|
|
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 [DCMTK::Package]
|
|
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 [DCMTK::Package] 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
|
+
# @yield [Tempfile] You can #write bits to this object to create the new
|
|
71
|
+
# file
|
|
72
|
+
# @return [DCMTK::Package] The created file
|
|
73
|
+
#
|
|
74
|
+
def self.create(ext = nil, &block)
|
|
75
|
+
tempfile = DCMTK::Utilities.tempfile(ext.to_s.downcase, &block)
|
|
76
|
+
|
|
77
|
+
new(tempfile.path, tempfile)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
attr_reader :path
|
|
81
|
+
attr_reader :tempfile
|
|
82
|
+
|
|
83
|
+
def initialize(input_path, tempfile = nil)
|
|
84
|
+
@path = input_path.to_s
|
|
85
|
+
@tempfile = tempfile
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_blob
|
|
89
|
+
File.binread(path)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Converts the DICOM file to the specified image format.
|
|
94
|
+
#
|
|
95
|
+
# @param format [Symbol] The output format (:png, :bmp, :jpeg)
|
|
96
|
+
# @return [self]
|
|
97
|
+
#
|
|
98
|
+
def convert(format: :png)
|
|
99
|
+
ext = ".#{format}"
|
|
100
|
+
|
|
101
|
+
if @tempfile
|
|
102
|
+
new_tempfile = DCMTK::Utilities.tempfile(ext)
|
|
103
|
+
new_path = new_tempfile.path
|
|
104
|
+
else
|
|
105
|
+
new_path = Pathname(path).sub_ext(ext).to_s
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
input_path = path.dup
|
|
109
|
+
|
|
110
|
+
DCMTK::Tool::Convert.new do |convert|
|
|
111
|
+
convert.format(format)
|
|
112
|
+
convert << input_path
|
|
113
|
+
convert << new_path
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if @tempfile
|
|
117
|
+
destroy!
|
|
118
|
+
@tempfile = new_tempfile
|
|
119
|
+
else
|
|
120
|
+
File.delete(path) unless path == new_path
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
path.replace new_path
|
|
124
|
+
|
|
125
|
+
self
|
|
126
|
+
rescue DCMTK::Error => e
|
|
127
|
+
new_tempfile.unlink if new_tempfile && @tempfile != new_tempfile
|
|
128
|
+
raise e
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
# Writes the temporary file out to either a file location (by passing in a
|
|
133
|
+
# String) or by passing in a Stream that you can #write(chunk) to
|
|
134
|
+
# repeatedly
|
|
135
|
+
#
|
|
136
|
+
# @param output_to [String, Pathname, #read] Some kind of stream object
|
|
137
|
+
# that needs to be read or a file path as a String
|
|
138
|
+
#
|
|
139
|
+
def write(output_to)
|
|
140
|
+
case output_to
|
|
141
|
+
when String, Pathname
|
|
142
|
+
FileUtils.copy_file path, output_to unless path == output_to.to_s
|
|
143
|
+
else
|
|
144
|
+
IO.copy_stream File.open(path, "rb"), output_to
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
##
|
|
149
|
+
# Destroys the tempfile (created by {.open}) if it exists.
|
|
150
|
+
#
|
|
151
|
+
def destroy!
|
|
152
|
+
if @tempfile
|
|
153
|
+
@tempfile.unlink
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/dcmtk/shell.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "timeout"
|
|
2
|
+
require "benchmark"
|
|
3
|
+
|
|
4
|
+
module DCMTK
|
|
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, DCMTK.whiny)
|
|
17
|
+
fail DCMTK::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_#{DCMTK.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(DCMTK.timeout)
|
|
54
|
+
Process.kill("TERM", thread.pid) rescue nil
|
|
55
|
+
Process.waitpid(thread.pid) rescue nil
|
|
56
|
+
raise Timeout::Error, "DCMTK 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: DCMTK.timeout)
|
|
66
|
+
[child.out, child.err, child.status]
|
|
67
|
+
rescue POSIX::Spawn::TimeoutExceeded
|
|
68
|
+
raise Timeout::Error, "DCMTK command timed out: #{command}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def log(command, &block)
|
|
72
|
+
value = nil
|
|
73
|
+
duration = Benchmark.realtime { value = block.call }
|
|
74
|
+
DCMTK.logger.debug "[%.2fs] %s" % [duration, command]
|
|
75
|
+
value
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module DCMTK
|
|
2
|
+
class Tool
|
|
3
|
+
class Convert < DCMTK::Tool
|
|
4
|
+
FORMATS = {
|
|
5
|
+
png: '+on', # PNG format
|
|
6
|
+
bmp: '+ob', # BMP format
|
|
7
|
+
jpeg: '+oj' # JPEG format
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(*args)
|
|
11
|
+
super(DCMTK.convert_cli, *args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Sets the output format for the conversion.
|
|
16
|
+
#
|
|
17
|
+
# @param type [Symbol] The format type (:png, :bmp, :jpeg)
|
|
18
|
+
# @return [self]
|
|
19
|
+
# @raise [ArgumentError] If the format is not supported
|
|
20
|
+
#
|
|
21
|
+
def format(type)
|
|
22
|
+
flag = FORMATS.fetch(type.to_sym) do
|
|
23
|
+
raise ArgumentError, "Unsupported format: #{type}. Use: #{FORMATS.keys.join(', ')}"
|
|
24
|
+
end
|
|
25
|
+
self << flag
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/dcmtk/tool.rb
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require "dcmtk/shell"
|
|
2
|
+
|
|
3
|
+
module DCMTK
|
|
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 DCMTK
|
|
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 = DCMTK::Tool::Convert.new { |b| b.version }
|
|
17
|
+
# puts version
|
|
18
|
+
#
|
|
19
|
+
# @return [DCMTK::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
|
+
# DCMTK::Tool::Convert.new(whiny: false) do |convert|
|
|
43
|
+
# convert.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, DCMTK.whiny) : options
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Executes the command that has been built up.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# convert = DCMTK::Tool::Convert.new
|
|
56
|
+
# convert.format(:png)
|
|
57
|
+
# convert << "path/to/file.dcm"
|
|
58
|
+
# convert << "output.png"
|
|
59
|
+
# convert.call # executes `dcmj2pnm +on path/to/file.dcm output.png`
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# convert = DCMTK::Tool::Convert.new
|
|
63
|
+
# # build the command
|
|
64
|
+
# convert.call do |stdout, stderr, status|
|
|
65
|
+
# # ...
|
|
66
|
+
# end
|
|
67
|
+
#
|
|
68
|
+
# @yield [Array] Optionally yields stdout, stderr, and exit status
|
|
69
|
+
#
|
|
70
|
+
# @return [String] Returns the output of the command
|
|
71
|
+
#
|
|
72
|
+
def call(*args)
|
|
73
|
+
options = args[-1].is_a?(Hash) ? args.pop : {}
|
|
74
|
+
whiny = args.fetch(0, @whiny)
|
|
75
|
+
|
|
76
|
+
options[:whiny] = whiny
|
|
77
|
+
options[:stderr] = false if block_given?
|
|
78
|
+
|
|
79
|
+
shell = DCMTK::Shell.new
|
|
80
|
+
stdout, stderr, status = shell.run(command, options)
|
|
81
|
+
yield stdout, stderr, status if block_given?
|
|
82
|
+
|
|
83
|
+
stdout.chomp("\n")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# The currently built-up command.
|
|
88
|
+
#
|
|
89
|
+
# @return [Array<String>]
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# convert = DCMTK::Tool::Convert.new
|
|
93
|
+
# convert.format(:png)
|
|
94
|
+
# convert << "input.dcm"
|
|
95
|
+
# convert << "output.png"
|
|
96
|
+
# convert.command #=> ["dcmj2pnm", "+on", "input.dcm", "output.png"]
|
|
97
|
+
#
|
|
98
|
+
def command
|
|
99
|
+
[*executable, *args]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def executable
|
|
103
|
+
exe = [name]
|
|
104
|
+
exe
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# Appends raw options, useful for appending file paths.
|
|
109
|
+
#
|
|
110
|
+
# @return [self]
|
|
111
|
+
#
|
|
112
|
+
def <<(arg)
|
|
113
|
+
args << arg.to_s
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# Merges a list of raw options.
|
|
119
|
+
#
|
|
120
|
+
# @return [self]
|
|
121
|
+
#
|
|
122
|
+
def merge!(new_args)
|
|
123
|
+
new_args.each { |arg| self << arg }
|
|
124
|
+
self
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Any undefined method will be transformed into a CLI option
|
|
129
|
+
#
|
|
130
|
+
# @example
|
|
131
|
+
# convert = DCMTK::Tool.new("dcmj2pnm")
|
|
132
|
+
# convert.verbose
|
|
133
|
+
# convert.command.join(" ") # => "dcmj2pnm --verbose"
|
|
134
|
+
#
|
|
135
|
+
def method_missing(name, *args)
|
|
136
|
+
option = "--#{name.to_s.tr('_', '-')}"
|
|
137
|
+
self << option
|
|
138
|
+
self.merge!(args)
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
require "dcmtk/tool/convert"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "tempfile"
|
|
2
|
+
|
|
3
|
+
module DCMTK
|
|
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
|
+
# DCMTK::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(["dcmtk", 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
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module DCMTK
|
|
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 = 1
|
|
13
|
+
PRE = nil
|
|
14
|
+
|
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/dcmtk.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'dcmtk/version'
|
|
2
|
+
require 'dcmtk/configuration'
|
|
3
|
+
|
|
4
|
+
module DCMTK
|
|
5
|
+
extend DCMTK::Configuration
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# Returns the CLI command to use for conversion.
|
|
9
|
+
# Uses configured cli if set, otherwise auto-detects (dcm2img preferred, dcmj2pnm fallback).
|
|
10
|
+
# Result is cached after first call.
|
|
11
|
+
#
|
|
12
|
+
# @return [String]
|
|
13
|
+
def self.convert_cli
|
|
14
|
+
return cli if cli
|
|
15
|
+
return @convert_cli if defined?(@convert_cli)
|
|
16
|
+
|
|
17
|
+
@convert_cli = DCMTK::Utilities.which('dcm2img') ? 'dcm2img' : 'dcmj2pnm'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Returns DCMTK CLI tool version.
|
|
22
|
+
#
|
|
23
|
+
# @return [String]
|
|
24
|
+
def self.cli_version
|
|
25
|
+
output = DCMTK::Tool::Convert.new { |c| c.version }
|
|
26
|
+
output[/\d+\.\d+\.\d+/]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Error < RuntimeError; end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require 'dcmtk/tool'
|
|
34
|
+
require 'dcmtk/package'
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dcmtk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- sanzstez
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-21 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 DCMTK 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/dcmtk.rb
|
|
66
|
+
- lib/dcmtk/configuration.rb
|
|
67
|
+
- lib/dcmtk/package.rb
|
|
68
|
+
- lib/dcmtk/shell.rb
|
|
69
|
+
- lib/dcmtk/tool.rb
|
|
70
|
+
- lib/dcmtk/tool/convert.rb
|
|
71
|
+
- lib/dcmtk/utilities.rb
|
|
72
|
+
- lib/dcmtk/version.rb
|
|
73
|
+
homepage: https://github.com/sanzstez/dcmtk
|
|
74
|
+
licenses:
|
|
75
|
+
- MIT
|
|
76
|
+
metadata: {}
|
|
77
|
+
post_install_message:
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '2.0'
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements:
|
|
92
|
+
- You must have DCMTK tools installed (dcmj2pnm)
|
|
93
|
+
rubygems_version: 3.5.22
|
|
94
|
+
signing_key:
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: Ruby adapter for DCMTK tools for DICOM medical files.
|
|
97
|
+
test_files: []
|