mini_magick 3.8.1 → 4.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|