mini_magick 3.8.1 → 4.9.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/lib/mini_gmagick.rb +1 -0
- data/lib/mini_magick/configuration.rb +198 -0
- data/lib/mini_magick/image/info.rb +192 -0
- data/lib/mini_magick/image.rb +456 -341
- data/lib/mini_magick/shell.rb +81 -0
- data/lib/mini_magick/tool/animate.rb +14 -0
- data/lib/mini_magick/tool/compare.rb +14 -0
- data/lib/mini_magick/tool/composite.rb +14 -0
- data/lib/mini_magick/tool/conjure.rb +14 -0
- data/lib/mini_magick/tool/convert.rb +14 -0
- data/lib/mini_magick/tool/display.rb +14 -0
- data/lib/mini_magick/tool/identify.rb +14 -0
- data/lib/mini_magick/tool/import.rb +14 -0
- data/lib/mini_magick/tool/magick.rb +14 -0
- data/lib/mini_magick/tool/mogrify.rb +14 -0
- data/lib/mini_magick/tool/mogrify_restricted.rb +15 -0
- data/lib/mini_magick/tool/montage.rb +14 -0
- data/lib/mini_magick/tool/stream.rb +14 -0
- data/lib/mini_magick/tool.rb +297 -0
- data/lib/mini_magick/utilities.rb +23 -50
- data/lib/mini_magick/version.rb +5 -5
- data/lib/mini_magick.rb +54 -65
- metadata +49 -51
- data/lib/mini_magick/command_builder.rb +0 -94
- data/lib/mini_magick/errors.rb +0 -4
- data/spec/files/actually_a_gif.jpg +0 -0
- data/spec/files/animation.gif +0 -0
- data/spec/files/composited.jpg +0 -0
- data/spec/files/erroneous.jpg +0 -0
- data/spec/files/layers.psd +0 -0
- data/spec/files/leaves (spaced).tiff +0 -0
- data/spec/files/not_an_image.php +0 -1
- data/spec/files/png.png +0 -0
- data/spec/files/simple-minus.gif +0 -0
- data/spec/files/simple.gif +0 -0
- data/spec/files/trogdor.jpg +0 -0
- data/spec/files/trogdor_capitalized.JPG +0 -0
- data/spec/lib/mini_magick/command_builder_spec.rb +0 -153
- data/spec/lib/mini_magick/image_spec.rb +0 -499
- data/spec/lib/mini_magick_spec.rb +0 -63
- data/spec/spec_helper.rb +0 -29
data/lib/mini_magick/image.rb
CHANGED
@@ -1,188 +1,184 @@
|
|
1
1
|
require 'tempfile'
|
2
|
-
require 'subexec'
|
3
2
|
require 'stringio'
|
4
3
|
require 'pathname'
|
4
|
+
require 'uri'
|
5
|
+
require 'open-uri'
|
6
|
+
|
7
|
+
require 'mini_magick/image/info'
|
8
|
+
require 'mini_magick/utilities'
|
5
9
|
|
6
10
|
module MiniMagick
|
7
11
|
class Image
|
8
|
-
# @return [String] The location of the current working file
|
9
|
-
attr_writer :path
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
13
|
+
##
|
14
|
+
# This is the primary loading method used by all of the other class
|
15
|
+
# methods.
|
16
|
+
#
|
17
|
+
# Use this to pass in a stream object. Must respond to #read(size) or be a
|
18
|
+
# binary string object (BLOBBBB)
|
19
|
+
#
|
20
|
+
# Probably easier to use the {.open} method if you want to open a file or a
|
21
|
+
# URL.
|
22
|
+
#
|
23
|
+
# @param stream [#read, String] Some kind of stream object that needs
|
24
|
+
# to be read or is a binary String blob
|
25
|
+
# @param ext [String] A manual extension to use for reading the file. Not
|
26
|
+
# required, but if you are having issues, give this a try.
|
27
|
+
# @return [MiniMagick::Image]
|
28
|
+
#
|
29
|
+
def self.read(stream, ext = nil)
|
30
|
+
if stream.is_a?(String)
|
31
|
+
stream = StringIO.new(stream)
|
32
|
+
end
|
33
|
+
|
34
|
+
create(ext) { |file| IO.copy_stream(stream, file) }
|
14
35
|
end
|
15
36
|
|
16
|
-
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
if File.respond_to?(:binread)
|
43
|
-
stream = StringIO.new File.binread(stream.path.to_s)
|
44
|
-
else
|
45
|
-
stream = StringIO.new File.open(stream.path.to_s, 'rb') { |f| f.read }
|
46
|
-
end
|
37
|
+
##
|
38
|
+
# Creates an image object from a binary string blob which contains raw
|
39
|
+
# pixel data (i.e. no header data).
|
40
|
+
#
|
41
|
+
# @param blob [String] Binary string blob containing raw pixel data.
|
42
|
+
# @param columns [Integer] Number of columns.
|
43
|
+
# @param rows [Integer] Number of rows.
|
44
|
+
# @param depth [Integer] Bit depth of the encoded pixel data.
|
45
|
+
# @param map [String] A code for the mapping of the pixel data. Example:
|
46
|
+
# 'gray' or 'rgb'.
|
47
|
+
# @param format [String] The file extension of the image format to be
|
48
|
+
# used when creating the image object.
|
49
|
+
# Defaults to 'png'.
|
50
|
+
# @return [MiniMagick::Image] The loaded image.
|
51
|
+
#
|
52
|
+
def self.import_pixels(blob, columns, rows, depth, map, format = 'png')
|
53
|
+
# Create an image object with the raw pixel data string:
|
54
|
+
create(".dat", false) { |f| f.write(blob) }.tap do |image|
|
55
|
+
output_path = image.path.sub(/\.\w+$/, ".#{format}")
|
56
|
+
# Use ImageMagick to convert the raw data file to an image file of the
|
57
|
+
# desired format:
|
58
|
+
MiniMagick::Tool::Convert.new do |convert|
|
59
|
+
convert.size "#{columns}x#{rows}"
|
60
|
+
convert.depth depth
|
61
|
+
convert << "#{map}:#{image.path}"
|
62
|
+
convert << output_path
|
47
63
|
end
|
48
64
|
|
49
|
-
|
50
|
-
while chunk = stream.read(8192)
|
51
|
-
f.write(chunk)
|
52
|
-
end
|
53
|
-
end
|
65
|
+
image.path.replace output_path
|
54
66
|
end
|
67
|
+
end
|
55
68
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
69
|
+
##
|
70
|
+
# Opens a specific image file either on the local file system or at a URI.
|
71
|
+
# Use this if you don't want to overwrite the image file.
|
72
|
+
#
|
73
|
+
# Extension is either guessed from the path or you can specify it as a
|
74
|
+
# second parameter.
|
75
|
+
#
|
76
|
+
# @param path_or_url [String] Either a local file path or a URL that
|
77
|
+
# open-uri can read
|
78
|
+
# @param ext [String] Specify the extension you want to read it as
|
79
|
+
# @param options [Hash] Specify options for the open method
|
80
|
+
# @return [MiniMagick::Image] The loaded image
|
81
|
+
#
|
82
|
+
def self.open(path_or_url, ext = nil, options = {})
|
83
|
+
options, ext = ext, nil if ext.is_a?(Hash)
|
61
84
|
|
62
|
-
|
63
|
-
# pixel data (i.e. no header data).
|
64
|
-
#
|
65
|
-
# @param blob [String] Binary string blob containing raw pixel data.
|
66
|
-
# @param columns [Integer] Number of columns.
|
67
|
-
# @param rows [Integer] Number of rows.
|
68
|
-
# @param depth [Integer] Bit depth of the encoded pixel data.
|
69
|
-
# @param map [String] A code for the mapping of the pixel data. Example:
|
70
|
-
# 'gray' or 'rgb'.
|
71
|
-
# @param format [String] The file extension of the image format to be
|
72
|
-
# used when creating the image object.
|
73
|
-
# Defaults to 'png'.
|
74
|
-
# @return [Image] The loaded image.
|
75
|
-
#
|
76
|
-
def import_pixels(blob, columns, rows, depth, map, format = 'png')
|
77
|
-
# Create an image object with the raw pixel data string:
|
78
|
-
image = create('.dat', false) { |f| f.write(blob) }
|
79
|
-
# Use ImageMagick to convert the raw data file to an image file of the
|
80
|
-
# desired format:
|
81
|
-
converted_image_path = image.path[0..-4] + format
|
82
|
-
arguments = ['-size', "#{columns}x#{rows}", '-depth', "#{depth}", "#{map}:#{image.path}", "#{converted_image_path}"]
|
83
|
-
# Example: convert -size 256x256 -depth 16 gray:blob.dat blob.png
|
84
|
-
cmd = CommandBuilder.new('convert', *arguments)
|
85
|
-
image.run(cmd)
|
86
|
-
# Update the image instance with the path of the properly formatted
|
87
|
-
# image, and return:
|
88
|
-
image.path = converted_image_path
|
89
|
-
image
|
90
|
-
end
|
85
|
+
uri = URI(path_or_url.to_s)
|
91
86
|
|
92
|
-
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
# If you pass in what looks like a URL, we require 'open-uri' before
|
100
|
-
# opening it.
|
101
|
-
#
|
102
|
-
# @param file_or_url [String] Either a local file path or a URL that
|
103
|
-
# open-uri can read
|
104
|
-
# @param ext [String] Specify the extension you want to read it as
|
105
|
-
# @return [Image] The loaded image
|
106
|
-
def open(file_or_url, ext = nil)
|
107
|
-
file_or_url = file_or_url.to_s # Force String... Hell or high water
|
108
|
-
if file_or_url.include?('://')
|
109
|
-
require 'open-uri'
|
110
|
-
ext ||= File.extname(URI.parse(file_or_url).path)
|
111
|
-
Kernel.open(file_or_url) do |f|
|
112
|
-
read(f, ext)
|
113
|
-
end
|
114
|
-
else
|
115
|
-
ext ||= File.extname(file_or_url)
|
116
|
-
File.open(file_or_url, 'rb') do |f|
|
117
|
-
read(f, ext)
|
118
|
-
end
|
119
|
-
end
|
87
|
+
ext ||= File.extname(uri.path)
|
88
|
+
ext.sub!(/:.*/, '') # hack for filenames or URLs that include a colon
|
89
|
+
|
90
|
+
if uri.is_a?(URI::HTTP) || uri.is_a?(URI::FTP)
|
91
|
+
uri.open(options) { |file| read(file, ext) }
|
92
|
+
else
|
93
|
+
File.open(uri.to_s, "rb", options) { |file| read(file, ext) }
|
120
94
|
end
|
95
|
+
end
|
121
96
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
97
|
+
##
|
98
|
+
# Used to create a new Image object data-copy. Not used to "paint" or
|
99
|
+
# that kind of thing.
|
100
|
+
#
|
101
|
+
# Takes an extension in a block and can be used to build a new Image
|
102
|
+
# object. Used by both {.open} and {.read} to create a new object. Ensures
|
103
|
+
# we have a good tempfile.
|
104
|
+
#
|
105
|
+
# @param ext [String] Specify the extension you want to read it as
|
106
|
+
# @param validate [Boolean] If false, skips validation of the created
|
107
|
+
# image. Defaults to true.
|
108
|
+
# @yield [Tempfile] You can #write bits to this object to create the new
|
109
|
+
# Image
|
110
|
+
# @return [MiniMagick::Image] The created image
|
111
|
+
#
|
112
|
+
def self.create(ext = nil, validate = MiniMagick.validate_on_create, &block)
|
113
|
+
tempfile = MiniMagick::Utilities.tempfile(ext.to_s.downcase, &block)
|
114
|
+
|
115
|
+
new(tempfile.path, tempfile).tap do |image|
|
116
|
+
image.validate! if validate
|
126
117
|
end
|
118
|
+
end
|
127
119
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
# @return [Image] The created image
|
141
|
-
def create(ext = nil, validate = MiniMagick.validate_on_create, &block)
|
142
|
-
tempfile = Tempfile.new(['mini_magick', ext.to_s.downcase])
|
143
|
-
tempfile.binmode
|
144
|
-
block.call(tempfile)
|
145
|
-
tempfile.close
|
146
|
-
|
147
|
-
image = new(tempfile.path, tempfile)
|
148
|
-
|
149
|
-
fail MiniMagick::Invalid if validate && !image.valid?
|
150
|
-
return image
|
151
|
-
ensure
|
152
|
-
tempfile.close if tempfile
|
120
|
+
##
|
121
|
+
# @private
|
122
|
+
# @!macro [attach] attribute
|
123
|
+
# @!attribute [r] $1
|
124
|
+
#
|
125
|
+
def self.attribute(name, key = name.to_s)
|
126
|
+
define_method(name) do |*args|
|
127
|
+
if args.any? && name != :resolution
|
128
|
+
mogrify { |b| b.send(name, *args) }
|
129
|
+
else
|
130
|
+
@info[key, *args]
|
131
|
+
end
|
153
132
|
end
|
154
133
|
end
|
155
134
|
|
156
|
-
|
135
|
+
##
|
136
|
+
# @return [String] The location of the current working file
|
137
|
+
#
|
138
|
+
attr_reader :path
|
139
|
+
##
|
140
|
+
# @return [Tempfile] The underlying temporary file
|
141
|
+
#
|
142
|
+
attr_reader :tempfile
|
143
|
+
|
144
|
+
##
|
145
|
+
# Create a new {MiniMagick::Image} object.
|
157
146
|
#
|
158
147
|
# _DANGER_: The file location passed in here is the *working copy*. That
|
159
|
-
# is, it gets *modified*.
|
160
|
-
#
|
161
|
-
#
|
162
|
-
#
|
163
|
-
# @
|
164
|
-
#
|
165
|
-
#
|
166
|
-
def initialize(input_path, tempfile = nil)
|
167
|
-
@path = input_path
|
148
|
+
# is, it gets *modified*. You can either copy it yourself or use {.open}
|
149
|
+
# which creates a temporary file for you and protects your original.
|
150
|
+
#
|
151
|
+
# @param input_path [String, Pathname] The location of an image file
|
152
|
+
# @yield [MiniMagick::Tool::Mogrify] If block is given, {#combine_options}
|
153
|
+
# is called.
|
154
|
+
#
|
155
|
+
def initialize(input_path, tempfile = nil, &block)
|
156
|
+
@path = input_path.to_s
|
168
157
|
@tempfile = tempfile
|
169
|
-
@info =
|
170
|
-
|
158
|
+
@info = MiniMagick::Image::Info.new(@path)
|
159
|
+
|
160
|
+
combine_options(&block) if block
|
171
161
|
end
|
172
162
|
|
173
|
-
def
|
174
|
-
|
175
|
-
@queue = MiniMagick::CommandBuilder.new('mogrify')
|
176
|
-
@info.clear
|
163
|
+
def ==(other)
|
164
|
+
self.class == other.class && signature == other.signature
|
177
165
|
end
|
166
|
+
alias eql? ==
|
178
167
|
|
179
|
-
def
|
180
|
-
|
181
|
-
@queue << MiniMagick::Utilities.path(@path)
|
182
|
-
run(@queue)
|
183
|
-
reset_queue
|
168
|
+
def hash
|
169
|
+
signature.hash
|
184
170
|
end
|
185
171
|
|
172
|
+
##
|
173
|
+
# Returns raw image data.
|
174
|
+
#
|
175
|
+
# @return [String] Binary string
|
176
|
+
#
|
177
|
+
def to_blob
|
178
|
+
File.binread(path)
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
186
182
|
# Checks to make sure that MiniMagick can read the file and understand it.
|
187
183
|
#
|
188
184
|
# This uses the 'identify' command line utility to check the file. If you
|
@@ -190,89 +186,184 @@ module MiniMagick
|
|
190
186
|
# 'identify' command and see if you can figure out what the issue is.
|
191
187
|
#
|
192
188
|
# @return [Boolean]
|
189
|
+
#
|
193
190
|
def valid?
|
194
|
-
|
191
|
+
validate!
|
195
192
|
true
|
196
193
|
rescue MiniMagick::Invalid
|
197
194
|
false
|
198
195
|
end
|
199
196
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
197
|
+
##
|
198
|
+
# Runs `identify` on the current image, and raises an error if it doesn't
|
199
|
+
# pass.
|
200
|
+
#
|
201
|
+
# @raise [MiniMagick::Invalid]
|
202
|
+
#
|
203
|
+
def validate!
|
204
|
+
identify
|
205
|
+
rescue MiniMagick::Error => error
|
206
|
+
raise MiniMagick::Invalid, error.message
|
204
207
|
end
|
205
208
|
|
206
|
-
|
207
|
-
#
|
208
|
-
#
|
209
|
+
##
|
210
|
+
# Returns the image format (e.g. "JPEG", "GIF").
|
211
|
+
#
|
212
|
+
# @return [String]
|
213
|
+
#
|
214
|
+
attribute :type, "format"
|
215
|
+
##
|
216
|
+
# @return [String]
|
217
|
+
#
|
218
|
+
attribute :mime_type
|
219
|
+
##
|
220
|
+
# @return [Integer]
|
221
|
+
#
|
222
|
+
attribute :width
|
223
|
+
##
|
224
|
+
# @return [Integer]
|
225
|
+
#
|
226
|
+
attribute :height
|
227
|
+
##
|
228
|
+
# @return [Array<Integer>]
|
229
|
+
#
|
230
|
+
attribute :dimensions
|
231
|
+
##
|
232
|
+
# Returns the file size of the image (in bytes).
|
233
|
+
#
|
234
|
+
# @return [Integer]
|
235
|
+
#
|
236
|
+
attribute :size
|
237
|
+
##
|
238
|
+
# Returns the file size in a human readable format.
|
239
|
+
#
|
240
|
+
# @return [String]
|
241
|
+
#
|
242
|
+
attribute :human_size
|
243
|
+
##
|
244
|
+
# @return [String]
|
245
|
+
#
|
246
|
+
attribute :colorspace
|
247
|
+
##
|
248
|
+
# @return [Hash]
|
249
|
+
#
|
250
|
+
attribute :exif
|
251
|
+
##
|
252
|
+
# Returns the resolution of the photo. You can optionally specify the
|
253
|
+
# units measurement.
|
254
|
+
#
|
255
|
+
# @example
|
256
|
+
# image.resolution("PixelsPerInch") #=> [250, 250]
|
257
|
+
# @see http://www.imagemagick.org/script/command-line-options.php#units
|
258
|
+
# @return [Array<Integer>]
|
259
|
+
#
|
260
|
+
attribute :resolution
|
261
|
+
##
|
262
|
+
# Returns the message digest of this image as a SHA-256, hexidecimal
|
263
|
+
# encoded string. This signature uniquely identifies the image and is
|
264
|
+
# convenient for determining if an image has been modified or whether two
|
265
|
+
# images are identical.
|
266
|
+
#
|
267
|
+
# @example
|
268
|
+
# image.signature #=> "60a7848c4ca6e36b8e2c5dea632ecdc29e9637791d2c59ebf7a54c0c6a74ef7e"
|
269
|
+
# @see http://www.imagemagick.org/api/signature.php
|
270
|
+
# @return [String]
|
271
|
+
#
|
272
|
+
attribute :signature
|
273
|
+
##
|
274
|
+
# Returns the information from `identify -verbose` in a Hash format, for
|
275
|
+
# ImageMagick.
|
276
|
+
#
|
277
|
+
# @return [Hash]
|
278
|
+
attribute :data
|
279
|
+
##
|
280
|
+
# Returns the information from `identify -verbose` in a Hash format, for
|
281
|
+
# GraphicsMagick.
|
282
|
+
#
|
283
|
+
# @return [Hash]
|
284
|
+
attribute :details
|
285
|
+
|
286
|
+
##
|
287
|
+
# Use this method if you want to access raw Identify's format API.
|
209
288
|
#
|
210
289
|
# @example
|
211
|
-
# image["
|
212
|
-
# image["
|
213
|
-
#
|
214
|
-
#
|
215
|
-
#
|
216
|
-
#
|
217
|
-
#
|
218
|
-
# image["EXIF:ExifVersion"] #=> "0220" (Can read anything from Exif)
|
219
|
-
#
|
220
|
-
# @param format [String] A format for the "identify" command
|
221
|
-
# @see http://www.imagemagick.org/script/command-line-options.php#format
|
222
|
-
# @return [String, Numeric, Array, Time, Object] Depends on the method
|
223
|
-
# called! Defaults to String for unknown commands
|
290
|
+
# image["%w %h"] #=> "250 450"
|
291
|
+
# image["%r"] #=> "DirectClass sRGB"
|
292
|
+
#
|
293
|
+
# @param value [String]
|
294
|
+
# @see http://www.imagemagick.org/script/escape.php
|
295
|
+
# @return [String]
|
296
|
+
#
|
224
297
|
def [](value)
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
File.size(path) # Do this because calling identify -format "%b" on an animated gif fails!
|
246
|
-
when 'original_at'
|
247
|
-
# Get the EXIF original capture as a Time object
|
248
|
-
Time.local(*self['EXIF:DateTimeOriginal'].split(/:|\s+/)) rescue nil
|
249
|
-
when /^EXIF\:/i
|
250
|
-
result = run_command('identify', '-format', "%[#{value}]", path).chomp
|
251
|
-
if result.include?(',')
|
252
|
-
read_character_data(result)
|
253
|
-
else
|
254
|
-
result
|
255
|
-
end
|
256
|
-
else
|
257
|
-
run_command('identify', '-format', value, path).split("\n")[0]
|
258
|
-
end
|
259
|
-
|
260
|
-
@info[value] = retrieved unless retrieved.nil?
|
261
|
-
@info[value]
|
298
|
+
@info[value.to_s]
|
299
|
+
end
|
300
|
+
alias info []
|
301
|
+
|
302
|
+
##
|
303
|
+
# Returns layers of the image. For example, JPEGs are 1-layered, but
|
304
|
+
# formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
|
305
|
+
#
|
306
|
+
# @example
|
307
|
+
# image = MiniMagick::Image.new("document.pdf")
|
308
|
+
# image.pages.each_with_index do |page, idx|
|
309
|
+
# page.write("page#{idx}.pdf")
|
310
|
+
# end
|
311
|
+
# @return [Array<MiniMagick::Image>]
|
312
|
+
#
|
313
|
+
def layers
|
314
|
+
layers_count = identify.lines.count
|
315
|
+
layers_count.times.map do |idx|
|
316
|
+
MiniMagick::Image.new("#{path}[#{idx}]")
|
317
|
+
end
|
262
318
|
end
|
319
|
+
alias pages layers
|
320
|
+
alias frames layers
|
263
321
|
|
264
|
-
|
265
|
-
#
|
322
|
+
##
|
323
|
+
# Returns a matrix of pixels from the image. The matrix is constructed as
|
324
|
+
# an array (1) of arrays (2) of arrays (3) of unsigned integers:
|
325
|
+
#
|
326
|
+
# 1) one for each row of pixels
|
327
|
+
# 2) one for each column of pixels
|
328
|
+
# 3) three elements in the range 0-255, one for each of the RGB color channels
|
266
329
|
#
|
267
|
-
#
|
268
|
-
#
|
330
|
+
# @example
|
331
|
+
# img = MiniMagick::Image.open 'image.jpg'
|
332
|
+
# pixels = img.get_pixels
|
333
|
+
# pixels[3][2][1] # the green channel value from the 4th-row, 3rd-column pixel
|
334
|
+
#
|
335
|
+
# It can also be called after applying transformations:
|
269
336
|
#
|
270
|
-
# @
|
271
|
-
#
|
272
|
-
|
273
|
-
|
337
|
+
# @example
|
338
|
+
# img = MiniMagick::Image.open 'image.jpg'
|
339
|
+
# img.crop '20x30+10+5'
|
340
|
+
# img.colorspace 'Gray'
|
341
|
+
# pixels = img.get_pixels
|
342
|
+
#
|
343
|
+
# In this example, all pixels in pix should now have equal R, G, and B values.
|
344
|
+
#
|
345
|
+
# @return [Array] Matrix of each color of each pixel
|
346
|
+
def get_pixels
|
347
|
+
convert = MiniMagick::Tool::Convert.new
|
348
|
+
convert << path
|
349
|
+
convert.depth(8)
|
350
|
+
convert << "RGB:-"
|
351
|
+
|
352
|
+
# Do not use `convert.call` here. We need the whole binary (unstripped) output here.
|
353
|
+
shell = MiniMagick::Shell.new
|
354
|
+
output, * = shell.run(convert.command)
|
355
|
+
|
356
|
+
pixels_array = output.unpack("C*")
|
357
|
+
pixels = pixels_array.each_slice(3).each_slice(width).to_a
|
358
|
+
|
359
|
+
# deallocate large intermediary objects
|
360
|
+
output.clear
|
361
|
+
pixels_array.clear
|
362
|
+
|
363
|
+
pixels
|
274
364
|
end
|
275
365
|
|
366
|
+
##
|
276
367
|
# This is used to change the format of the image. That is, from "tiff to
|
277
368
|
# jpg" or something like that. Once you run it, the instance is pointing to
|
278
369
|
# a new file with a new extension!
|
@@ -293,169 +384,193 @@ module MiniMagick
|
|
293
384
|
# @param page [Integer] If this is an animated gif, say which 'page' you
|
294
385
|
# want with an integer. Default 0 will convert only the first page; 'nil'
|
295
386
|
# will convert all pages.
|
296
|
-
# @
|
297
|
-
|
298
|
-
|
387
|
+
# @param read_opts [Hash] Any read options to be passed to ImageMagick
|
388
|
+
# for example: image.format('jpg', page, {density: '300'})
|
389
|
+
# @yield [MiniMagick::Tool::Convert] It optionally yields the command,
|
390
|
+
# if you want to add something.
|
391
|
+
# @return [self]
|
392
|
+
#
|
393
|
+
def format(format, page = 0, read_opts={})
|
394
|
+
if @tempfile
|
395
|
+
new_tempfile = MiniMagick::Utilities.tempfile(".#{format}")
|
396
|
+
new_path = new_tempfile.path
|
397
|
+
else
|
398
|
+
new_path = Pathname(path).sub_ext(".#{format}").to_s
|
399
|
+
end
|
400
|
+
|
401
|
+
input_path = path.dup
|
402
|
+
input_path << "[#{page}]" if page && !layer?
|
299
403
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
404
|
+
MiniMagick::Tool::Convert.new do |convert|
|
405
|
+
read_opts.each do |opt, val|
|
406
|
+
convert.send(opt.to_s, val)
|
407
|
+
end
|
408
|
+
convert << input_path
|
409
|
+
yield convert if block_given?
|
410
|
+
convert << new_path
|
411
|
+
end
|
412
|
+
|
413
|
+
if @tempfile
|
414
|
+
destroy!
|
415
|
+
@tempfile = new_tempfile
|
416
|
+
else
|
417
|
+
File.delete(path) unless path == new_path || layer?
|
418
|
+
end
|
304
419
|
|
305
|
-
|
420
|
+
path.replace new_path
|
421
|
+
@info.clear
|
306
422
|
|
307
|
-
self
|
423
|
+
self
|
424
|
+
end
|
308
425
|
|
309
|
-
|
426
|
+
##
|
427
|
+
# You can use multiple commands together using this method. Very easy to
|
428
|
+
# use!
|
429
|
+
#
|
430
|
+
# @example
|
431
|
+
# image.combine_options do |c|
|
432
|
+
# c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
|
433
|
+
# c.thumbnail "300x500>"
|
434
|
+
# c.background "blue"
|
435
|
+
# end
|
436
|
+
#
|
437
|
+
# @yield [MiniMagick::Tool::Mogrify]
|
438
|
+
# @see http://www.imagemagick.org/script/mogrify.php
|
439
|
+
# @return [self]
|
440
|
+
#
|
441
|
+
def combine_options(&block)
|
442
|
+
mogrify(&block)
|
443
|
+
end
|
310
444
|
|
311
|
-
|
312
|
-
|
445
|
+
##
|
446
|
+
# If an unknown method is called then it is sent through the mogrify
|
447
|
+
# program.
|
448
|
+
#
|
449
|
+
# @see http://www.imagemagick.org/script/mogrify.php
|
450
|
+
# @return [self]
|
451
|
+
#
|
452
|
+
def method_missing(name, *args)
|
453
|
+
mogrify do |builder|
|
454
|
+
builder.send(name, *args)
|
313
455
|
end
|
314
456
|
end
|
315
457
|
|
316
|
-
|
317
|
-
|
318
|
-
def collapse!
|
319
|
-
run_command('mogrify', '-quality', '100', "#{path}[0]")
|
458
|
+
def respond_to_missing?(method_name, include_private = false)
|
459
|
+
MiniMagick::Tool::Mogrify.option_methods.include?(method_name.to_s)
|
320
460
|
end
|
321
461
|
|
462
|
+
##
|
322
463
|
# Writes the temporary file out to either a file location (by passing in a
|
323
464
|
# String) or by passing in a Stream that you can #write(chunk) to
|
324
465
|
# repeatedly
|
325
466
|
#
|
326
|
-
# @param output_to [
|
327
|
-
# to be read or a file path as a String
|
328
|
-
#
|
329
|
-
# you get a success boolean. If its a stream, you get it back.
|
467
|
+
# @param output_to [String, Pathname, #read] Some kind of stream object
|
468
|
+
# that needs to be read or a file path as a String
|
469
|
+
#
|
330
470
|
def write(output_to)
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
'identify', MiniMagick::Utilities.path(output_to.to_s)
|
338
|
-
) # Verify that we have a good image
|
339
|
-
end
|
340
|
-
else # stream
|
341
|
-
File.open(path, 'rb') do |f|
|
342
|
-
f.binmode
|
343
|
-
while chunk = f.read(8192)
|
344
|
-
output_to.write(chunk)
|
471
|
+
case output_to
|
472
|
+
when String, Pathname
|
473
|
+
if layer?
|
474
|
+
MiniMagick::Tool::Convert.new do |builder|
|
475
|
+
builder << path
|
476
|
+
builder << output_to
|
345
477
|
end
|
478
|
+
else
|
479
|
+
FileUtils.copy_file path, output_to unless path == output_to.to_s
|
346
480
|
end
|
347
|
-
|
481
|
+
else
|
482
|
+
IO.copy_stream File.open(path, "rb"), output_to
|
348
483
|
end
|
349
484
|
end
|
350
485
|
|
351
|
-
|
352
|
-
# @
|
353
|
-
|
354
|
-
|
486
|
+
##
|
487
|
+
# @example
|
488
|
+
# first_image = MiniMagick::Image.open "first.jpg"
|
489
|
+
# second_image = MiniMagick::Image.open "second.jpg"
|
490
|
+
# result = first_image.composite(second_image) do |c|
|
491
|
+
# c.compose "Over" # OverCompositeOp
|
492
|
+
# c.geometry "+20+20" # copy second_image onto first_image from (20, 20)
|
493
|
+
# end
|
494
|
+
# result.write "output.jpg"
|
495
|
+
#
|
496
|
+
# @see http://www.imagemagick.org/script/composite.php
|
497
|
+
#
|
498
|
+
def composite(other_image, output_extension = type.downcase, mask = nil)
|
499
|
+
output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
|
500
|
+
|
501
|
+
MiniMagick::Tool::Composite.new do |composite|
|
502
|
+
yield composite if block_given?
|
503
|
+
composite << other_image.path
|
504
|
+
composite << path
|
505
|
+
composite << mask.path if mask
|
506
|
+
composite << output_tempfile.path
|
507
|
+
end
|
355
508
|
|
356
|
-
|
357
|
-
f.binmode
|
358
|
-
f.read
|
359
|
-
ensure
|
360
|
-
f.close if f
|
509
|
+
Image.new(output_tempfile.path, output_tempfile)
|
361
510
|
end
|
362
511
|
|
363
|
-
|
364
|
-
|
365
|
-
|
512
|
+
##
|
513
|
+
# Collapse images with sequences to the first frame (i.e. animated gifs) and
|
514
|
+
# preserve quality.
|
515
|
+
#
|
516
|
+
# @param frame [Integer] The frame to which to collapse to, defaults to `0`.
|
517
|
+
# @return [self]
|
518
|
+
#
|
519
|
+
def collapse!(frame = 0)
|
520
|
+
mogrify(frame) { |builder| builder.quality(100) }
|
366
521
|
end
|
367
522
|
|
368
|
-
|
369
|
-
#
|
523
|
+
##
|
524
|
+
# Destroys the tempfile (created by {.open}) if it exists.
|
370
525
|
#
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
526
|
+
def destroy!
|
527
|
+
if @tempfile
|
528
|
+
FileUtils.rm_f @tempfile.path.sub(/mpc$/, "cache") if @tempfile.path.end_with?(".mpc")
|
529
|
+
@tempfile.unlink
|
530
|
+
end
|
375
531
|
end
|
376
532
|
|
377
|
-
|
378
|
-
#
|
533
|
+
##
|
534
|
+
# Runs `identify` on itself. Accepts an optional block for adding more
|
535
|
+
# options to `identify`.
|
379
536
|
#
|
380
537
|
# @example
|
381
|
-
# image
|
382
|
-
#
|
383
|
-
#
|
384
|
-
#
|
385
|
-
#
|
538
|
+
# image = MiniMagick::Image.open("image.jpg")
|
539
|
+
# image.identify do |b|
|
540
|
+
# b.verbose
|
541
|
+
# end # runs `identify -verbose image.jpg`
|
542
|
+
# @return [String] Output from `identify`
|
543
|
+
# @yield [MiniMagick::Tool::Identify]
|
386
544
|
#
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
@command_queued = true
|
545
|
+
def identify
|
546
|
+
MiniMagick::Tool::Identify.new do |builder|
|
547
|
+
yield builder if block_given?
|
548
|
+
builder << path
|
392
549
|
end
|
393
550
|
end
|
394
551
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
second_tempfile.close
|
552
|
+
# @private
|
553
|
+
def run_command(tool_name, *args)
|
554
|
+
MiniMagick::Tool.const_get(tool_name.capitalize).new do |builder|
|
555
|
+
args.each do |arg|
|
556
|
+
builder << arg
|
557
|
+
end
|
402
558
|
end
|
403
|
-
|
404
|
-
command = CommandBuilder.new('composite')
|
405
|
-
block.call(command) if block
|
406
|
-
command.push(other_image.path)
|
407
|
-
command.push(path)
|
408
|
-
command.push(mask.path) unless mask.nil?
|
409
|
-
command.push(second_tempfile.path)
|
410
|
-
|
411
|
-
run(command)
|
412
|
-
Image.new(second_tempfile.path, second_tempfile)
|
413
559
|
end
|
414
560
|
|
415
|
-
def
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
args.unshift '-ping' # -ping "efficiently determine image characteristics."
|
420
|
-
args.unshift '-quiet' if MiniMagick.mogrify? && !MiniMagick.debug # graphicsmagick has no -quiet option.
|
561
|
+
def mogrify(page = nil)
|
562
|
+
MiniMagick::Tool::MogrifyRestricted.new do |builder|
|
563
|
+
yield builder if block_given?
|
564
|
+
builder << (page ? "#{path}[#{page}]" : path)
|
421
565
|
end
|
422
566
|
|
423
|
-
|
424
|
-
end
|
425
|
-
|
426
|
-
def run(command_builder)
|
427
|
-
command = command_builder.command
|
428
|
-
|
429
|
-
sub = Subexec.run(command, :timeout => MiniMagick.timeout)
|
430
|
-
|
431
|
-
if sub.exitstatus != 0
|
432
|
-
# Clean up after ourselves in case of an error
|
433
|
-
destroy!
|
434
|
-
|
435
|
-
# Raise the appropriate error
|
436
|
-
if sub.output =~ /no decode delegate/i || sub.output =~ /did not return an image/i
|
437
|
-
fail Invalid, sub.output
|
438
|
-
else
|
439
|
-
# TODO: should we do something different if the command times out ...?
|
440
|
-
# its definitely better for logging.. Otherwise we don't really know
|
441
|
-
fail Error, "Command (#{command.inspect.gsub("\\", "")}) failed: #{{ :status_code => sub.exitstatus, :output => sub.output }.inspect}"
|
442
|
-
end
|
443
|
-
else
|
444
|
-
sub.output
|
445
|
-
end
|
446
|
-
end
|
567
|
+
@info.clear
|
447
568
|
|
448
|
-
|
449
|
-
return if @tempfile.nil?
|
450
|
-
File.unlink(@path) if File.exist?(@path)
|
451
|
-
@tempfile = nil
|
569
|
+
self
|
452
570
|
end
|
453
571
|
|
454
|
-
|
455
|
-
|
456
|
-
# Sometimes we get back a list of character values
|
457
|
-
def read_character_data(string)
|
458
|
-
string.scan(/\d+/).map(&:to_i).map(&:chr).join
|
572
|
+
def layer?
|
573
|
+
path =~ /\[\d+\]$/
|
459
574
|
end
|
460
575
|
end
|
461
576
|
end
|