mini_magick 3.8.1 → 4.0.0.rc

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.

Potentially problematic release.


This version of mini_magick might be problematic. Click here for more details.

Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mini_gmagick.rb +2 -1
  3. data/lib/mini_magick.rb +43 -65
  4. data/lib/mini_magick/configuration.rb +136 -0
  5. data/lib/mini_magick/image.rb +356 -336
  6. data/lib/mini_magick/image/info.rb +104 -0
  7. data/lib/mini_magick/logger.rb +40 -0
  8. data/lib/mini_magick/shell.rb +46 -0
  9. data/lib/mini_magick/tool.rb +233 -0
  10. data/lib/mini_magick/tool/animate.rb +14 -0
  11. data/lib/mini_magick/tool/compare.rb +14 -0
  12. data/lib/mini_magick/tool/composite.rb +14 -0
  13. data/lib/mini_magick/tool/conjure.rb +14 -0
  14. data/lib/mini_magick/tool/convert.rb +14 -0
  15. data/lib/mini_magick/tool/display.rb +14 -0
  16. data/lib/mini_magick/tool/identify.rb +14 -0
  17. data/lib/mini_magick/tool/import.rb +14 -0
  18. data/lib/mini_magick/tool/mogrify.rb +14 -0
  19. data/lib/mini_magick/tool/montage.rb +14 -0
  20. data/lib/mini_magick/tool/stream.rb +14 -0
  21. data/lib/mini_magick/utilities.rb +23 -50
  22. data/lib/mini_magick/version.rb +6 -6
  23. data/spec/fixtures/animation.gif +0 -0
  24. data/spec/fixtures/default.jpg +0 -0
  25. data/spec/fixtures/exif.jpg +0 -0
  26. data/spec/fixtures/image.psd +0 -0
  27. data/spec/fixtures/not_an_image.rb +1 -0
  28. data/spec/lib/mini_magick/configuration_spec.rb +66 -0
  29. data/spec/lib/mini_magick/image_spec.rb +318 -410
  30. data/spec/lib/mini_magick/shell_spec.rb +66 -0
  31. data/spec/lib/mini_magick/tool_spec.rb +90 -0
  32. data/spec/lib/mini_magick/utilities_spec.rb +17 -0
  33. data/spec/lib/mini_magick_spec.rb +23 -47
  34. data/spec/spec_helper.rb +17 -25
  35. data/spec/support/helpers.rb +37 -0
  36. metadata +42 -76
  37. data/lib/mini_magick/command_builder.rb +0 -94
  38. data/lib/mini_magick/errors.rb +0 -4
  39. data/spec/files/actually_a_gif.jpg +0 -0
  40. data/spec/files/animation.gif +0 -0
  41. data/spec/files/composited.jpg +0 -0
  42. data/spec/files/erroneous.jpg +0 -0
  43. data/spec/files/layers.psd +0 -0
  44. data/spec/files/leaves (spaced).tiff +0 -0
  45. data/spec/files/not_an_image.php +0 -1
  46. data/spec/files/png.png +0 -0
  47. data/spec/files/simple-minus.gif +0 -0
  48. data/spec/files/simple.gif +0 -0
  49. data/spec/files/trogdor.jpg +0 -0
  50. data/spec/files/trogdor_capitalized.JPG +0 -0
  51. data/spec/lib/mini_magick/command_builder_spec.rb +0 -153
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4f4ae6b4f7d224af6275ad37b108c978f3fd8817
4
- data.tar.gz: 69d6e3f3b1289af2dd54a0309166e71fce1e2f45
3
+ metadata.gz: 2e11b9e22bbabb55096ee32c8635e5302459b234
4
+ data.tar.gz: ed3df8df5bd3c55605760ce4d68eb070180fd890
5
5
  SHA512:
6
- metadata.gz: 248c08f2af654156c2a422bb3bb3e48a9b97087bab6dacace044bbc2175465c9a2e5de09eac05cb4110cc4efcc8a47f25424f03f6effa6df2cbb2fda85524557
7
- data.tar.gz: 3c000c0861b8e66312d875b8858cdc3fe0b32c842332b70c979675acbf9b4fc62fcb4732c5c49db96240e0bf400974a90ee01e90a34b1c3d817eaf60444030fb
6
+ metadata.gz: e97b6cce0e021ab9f21a31e74e9b8afe9aca5e95c722fcd1230d31a3ece8310bed90fe91f2dc131a783a3e9c987217cc962b6fc296926c6f73fbb8ab263ce06d
7
+ data.tar.gz: 7f489774f7ce93a2e63d5fce3d12ddea5f70aa2a98b6343ff5f49df3cf66f67d9f8fbcd135f9eb9d3214e5160af7bfcc82d425895e5491dbe5194228b66a2b50
@@ -1,2 +1,3 @@
1
1
  require 'mini_magick'
2
- MiniMagick.processor = :gm
2
+
3
+ MiniMagick.processor = :graphicsmagick
@@ -1,75 +1,53 @@
1
- require 'mini_magick/command_builder'
2
- require 'mini_magick/errors'
1
+ require 'mini_magick/configuration'
2
+ require 'mini_magick/tool'
3
3
  require 'mini_magick/image'
4
- require 'mini_magick/utilities'
5
4
 
6
5
  module MiniMagick
7
- @validate_on_create = true
8
- @validate_on_write = true
9
6
 
10
- class << self
11
- attr_accessor :processor
12
- attr_accessor :processor_path
13
- attr_accessor :timeout
14
- attr_accessor :debug
15
- attr_accessor :validate_on_create
16
- attr_accessor :validate_on_write
17
-
18
- ##
19
- # Tries to detect the current processor based if any of the processors
20
- # exist. Mogrify have precedence over gm by default.
21
- #
22
- # === Returns
23
- # * [Symbol] The detected procesor
24
- def processor
25
- @processor ||= [:mogrify, :gm].detect do |processor|
26
- MiniMagick::Utilities.which(processor.to_s)
27
- end
28
- end
7
+ extend MiniMagick::Configuration
8
+
9
+ ##
10
+ # You might want to execute only certain blocks of processing with a
11
+ # different CLI, because for example that CLI does that particular thing
12
+ # faster. After the block CLI resets to its previous value.
13
+ #
14
+ # @example
15
+ # MiniMagick.with_cli :graphicsmagick do
16
+ # # operations that are better done with GraphicsMagick
17
+ # end
18
+ def self.with_cli(cli)
19
+ old_cli = self.cli
20
+ self.cli = cli
21
+ yield
22
+ self.cli = old_cli
23
+ end
29
24
 
30
- ##
31
- # Discovers the imagemagick version based on mogrify's output.
32
- #
33
- # === Returns
34
- # * The imagemagick version
35
- def image_magick_version
36
- @@version ||= Gem::Version.create(`mogrify --version`.split(' ')[2].split('-').first)
37
- end
25
+ ##
26
+ # Checks whether the CLI used is ImageMagick.
27
+ #
28
+ # @return [Boolean]
29
+ def self.imagemagick?
30
+ cli == :imagemagick
31
+ end
38
32
 
39
- ##
40
- # The minimum allowed imagemagick version
41
- #
42
- # === Returns
43
- # * The minimum imagemagick version
44
- def minimum_image_magick_version
45
- @@minimum_version ||= Gem::Version.create('6.6.3')
46
- end
33
+ ##
34
+ # Checks whether the CLI used is GraphicsMagick.
35
+ #
36
+ # @return [Boolean]
37
+ def self.graphicsmagick?
38
+ cli == :graphicsmagick
39
+ end
47
40
 
48
- ##
49
- # Checks whether the imagemagick's version is valid
50
- #
51
- # === Returns
52
- # * [Boolean]
53
- def valid_version_installed?
54
- image_magick_version >= minimum_image_magick_version
55
- end
41
+ ##
42
+ # Returns ImageMagick's/GraphicsMagick's version.
43
+ #
44
+ # @return [String]
45
+ def self.cli_version
46
+ output = MiniMagick::Tool::Identify.new(&:version)
47
+ output[/\d+\.\d+\.\d+(-\d+)?/]
48
+ end
56
49
 
57
- ##
58
- # Checks whether the current processory is mogrify.
59
- #
60
- # === Returns
61
- # * [Boolean]
62
- def mogrify?
63
- processor && processor.to_sym == :mogrify
64
- end
50
+ class Error < RuntimeError; end
51
+ class Invalid < StandardError; end
65
52
 
66
- ##
67
- # Checks whether the current processor is graphicsmagick.
68
- #
69
- # === Returns
70
- # * [Boolean]
71
- def gm?
72
- processor && processor.to_sym == :gm
73
- end
74
- end
75
53
  end
@@ -0,0 +1,136 @@
1
+ require 'mini_magick/utilities'
2
+
3
+ module MiniMagick
4
+ module Configuration
5
+
6
+ ##
7
+ # Set whether you want to use [ImageMagick](http://www.imagemagick.org) or
8
+ # [GraphicsMagick](http://www.graphicsmagick.org).
9
+ #
10
+ # @return [Symbol] `:imagemagick` or `:minimagick`
11
+ #
12
+ attr_accessor :cli
13
+ # @private (for backwards compatibility)
14
+ attr_accessor :processor
15
+
16
+ ##
17
+ # If you don't have the CLI tools in your PATH, you can set the path to the
18
+ # executables.
19
+ #
20
+ # @return [String]
21
+ #
22
+ attr_accessor :cli_path
23
+ # @private (for backwards compatibility)
24
+ attr_accessor :processor_path
25
+
26
+ ##
27
+ # If you don't want commands to take too long, you can set a timeout (in
28
+ # seconds).
29
+ #
30
+ # @return [Integer]
31
+ #
32
+ attr_accessor :timeout
33
+ ##
34
+ # When set to `true`, it outputs each command to STDOUT in their shell
35
+ # version.
36
+ #
37
+ # @return [Boolean]
38
+ #
39
+ attr_accessor :debug
40
+ ##
41
+ # Logger for {#debug}, default is `MiniMagick::Logger.new(STDOUT)`, but
42
+ # you can override it, for example if you want the logs to be written to
43
+ # a file.
44
+ #
45
+ # @return [Logger]
46
+ #
47
+ attr_accessor :logger
48
+ ##
49
+ # If set to `true`, it will `identify` every newly created image, and raise
50
+ # `MiniMagick::Invalid` if the image is not valid. Useful for validating
51
+ # user input, although it adds a bit of overhead. Defaults to `true`.
52
+ #
53
+ # @return [Boolean]
54
+ #
55
+ attr_accessor :validate_on_create
56
+ ##
57
+ # If set to `true`, it will `identify` every image that gets written (with
58
+ # {MiniMagick::Image#write}), and raise `MiniMagick::Invalid` if the image
59
+ # is not valid. Useful for validating that processing was sucessful,
60
+ # although it adds a bit of overhead. Defaults to `true`.
61
+ #
62
+ # @return [Boolean]
63
+ #
64
+ attr_accessor :validate_on_write
65
+
66
+ def self.extended(base)
67
+ base.validate_on_create = true
68
+ base.validate_on_write = true
69
+ end
70
+
71
+ ##
72
+ # @yield [self]
73
+ # @example
74
+ # MiniMagick.configure do |config|
75
+ # config.cli = :graphicsmagick
76
+ # config.timeout = 5
77
+ # end
78
+ #
79
+ def configure
80
+ yield self
81
+ end
82
+
83
+ def processor
84
+ @processor ||= ["mogrify", "gm"].detect do |processor|
85
+ MiniMagick::Utilities.which(processor)
86
+ end
87
+ end
88
+
89
+ def processor=(processor)
90
+ @processor = processor.to_s
91
+
92
+ unless ["mogrify", "gm"].include?(@processor)
93
+ raise ArgumentError,
94
+ "processor has to be set to either \"mogrify\" or \"gm\"" \
95
+ ", was set to #{@processor.inspect}"
96
+ end
97
+
98
+ reload_tools
99
+ end
100
+
101
+ def cli
102
+ @cli ||
103
+ case processor.to_s
104
+ when "mogrify" then :imagemagick
105
+ when "gm" then :graphicsmagick
106
+ end
107
+ end
108
+
109
+ def cli=(value)
110
+ @cli = value
111
+
112
+ unless [:imagemagick, :graphicsmagick].include?(@cli)
113
+ raise ArgumentError,
114
+ "CLI has to be set to either :imagemagick or :graphicsmagick" \
115
+ ", was set to #{@cli.inspect}"
116
+ end
117
+
118
+ reload_tools
119
+ end
120
+
121
+ def cli_path
122
+ @cli_path || @processor_path
123
+ end
124
+
125
+ def logger
126
+ @logger || MiniMagick::Logger.new($stdout)
127
+ end
128
+
129
+ private
130
+
131
+ def reload_tools
132
+ MiniMagick::Tool::OptionMethods.instances.each(&:reload_methods)
133
+ end
134
+
135
+ end
136
+ end
@@ -1,188 +1,164 @@
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
- def path
12
- run_queue if @command_queued
13
- MiniMagick::Utilities.path(@path)
14
- end
15
-
16
- # Class Methods
17
- # -------------
18
- class << self
19
- # This is the primary loading method used by all of the other class
20
- # methods.
21
- #
22
- # Use this to pass in a stream object. Must respond to Object#read(size)
23
- # or be a binary string object (BLOBBBB)
24
- #
25
- # As a change from the old API, please try and use IOStream objects. They
26
- # are much, much better and more efficient!
27
- #
28
- # Probably easier to use the #open method if you want to open a file or a
29
- # URL.
30
- #
31
- # @param stream [IOStream, String] Some kind of stream object that needs
32
- # to be read or is a binary String blob!
33
- # @param ext [String] A manual extension to use for reading the file. Not
34
- # required, but if you are having issues, give this a try.
35
- # @return [Image]
36
- def read(stream, ext = nil)
37
- if stream.is_a?(String)
38
- stream = StringIO.new(stream)
39
- elsif stream.is_a?(StringIO)
40
- # Do nothing, we want a StringIO-object
41
- elsif stream.respond_to? :path
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
47
- end
48
-
49
- create(ext) do |f|
50
- while chunk = stream.read(8192)
51
- f.write(chunk)
52
- end
53
- end
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)
54
32
  end
55
33
 
56
- # @deprecated Please use Image.read instead!
57
- def from_blob(blob, ext = nil)
58
- warn 'Warning: MiniMagick::Image.from_blob method is deprecated. Instead, please use Image.read'
59
- create(ext) { |f| f.write(blob) }
60
- end
34
+ create(ext) { |file| IO.copy_stream(stream, file) }
35
+ end
61
36
 
62
- # Creates an image object from a binary string blob which contains raw
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) }
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}")
79
56
  # Use ImageMagick to convert the raw data file to an image file of the
80
57
  # 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
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
63
+ end
64
+
65
+ image.path.replace output_path
90
66
  end
67
+ end
91
68
 
92
- # Opens a specific image file either on the local file system or at a URI.
93
- #
94
- # Use this if you don't want to overwrite the image file.
95
- #
96
- # Extension is either guessed from the path or you can specify it as a
97
- # second parameter.
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
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
+ # @return [MiniMagick::Image] The loaded image
80
+ #
81
+ def self.open(path_or_url, ext = nil)
82
+ ext ||=
83
+ if path_or_url.to_s =~ URI.regexp
84
+ File.extname(URI(path_or_url).path)
114
85
  else
115
- ext ||= File.extname(file_or_url)
116
- File.open(file_or_url, 'rb') do |f|
117
- read(f, ext)
118
- end
86
+ File.extname(path_or_url)
119
87
  end
88
+
89
+ Kernel.open(path_or_url, "rb") do |file|
90
+ read(file, ext)
120
91
  end
92
+ end
93
+
94
+ ##
95
+ # Used to create a new Image object data-copy. Not used to "paint" or
96
+ # that kind of thing.
97
+ #
98
+ # Takes an extension in a block and can be used to build a new Image
99
+ # object. Used by both {.open} and {.read} to create a new object. Ensures
100
+ # we have a good tempfile.
101
+ #
102
+ # @param ext [String] Specify the extension you want to read it as
103
+ # @param validate [Boolean] If false, skips validation of the created
104
+ # image. Defaults to true.
105
+ # @yield [Tempfile] You can #write bits to this object to create the new
106
+ # Image
107
+ # @return [MiniMagick::Image] The created image
108
+ #
109
+ def self.create(ext = nil, validate = MiniMagick.validate_on_create, &block)
110
+ tempfile = MiniMagick::Utilities.tempfile(ext.to_s.downcase, &block)
121
111
 
122
- # @deprecated Please use MiniMagick::Image.open(file_or_url) now
123
- def from_file(file, ext = nil)
124
- warn 'Warning: MiniMagick::Image.from_file is now deprecated. Please use Image.open'
125
- open(file, ext)
112
+ new(tempfile.path, tempfile).tap do |image|
113
+ image.validate! if validate
126
114
  end
115
+ end
127
116
 
128
- # Used to create a new Image object data-copy. Not used to "paint" or
129
- # that kind of thing.
130
- #
131
- # Takes an extension in a block and can be used to build a new Image
132
- # object. Used by both #open and #read to create a new object! Ensures we
133
- # have a good tempfile!
134
- #
135
- # @param ext [String] Specify the extension you want to read it as
136
- # @param validate [Boolean] If false, skips validation of the created
137
- # image. Defaults to true.
138
- # @yield [IOStream] You can #write bits to this object to create the new
139
- # Image
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
117
+ ##
118
+ # @private
119
+ # @!macro [attach] attribute
120
+ # @!attribute [r] $1
121
+ #
122
+ def self.attribute(name, key = name.to_s)
123
+ define_method(name) do |*args|
124
+ @info[key, *args]
153
125
  end
154
126
  end
155
127
 
156
- # Create a new MiniMagick::Image object
128
+ ##
129
+ # @return [String] The location of the current working file
130
+ #
131
+ attr_reader :path
132
+
133
+ ##
134
+ # Create a new {MiniMagick::Image} object.
157
135
  #
158
136
  # _DANGER_: The file location passed in here is the *working copy*. That
159
- # is, it gets *modified*. you can either copy it yourself or use the
160
- # MiniMagick::Image.open(path) method which creates a temporary file for
161
- # you and protects your original!
137
+ # is, it gets *modified*. You can either copy it yourself or use {.open}
138
+ # which creates a temporary file for you and protects your original.
162
139
  #
163
140
  # @param input_path [String] The location of an image file
164
- # @todo Allow this to accept a block that can pass off to
165
- # Image#combine_options
166
- def initialize(input_path, tempfile = nil)
141
+ # @yield [MiniMagick::Tool::Mogrify] If block is given, {#combine_options}
142
+ # is called.
143
+ #
144
+ def initialize(input_path, tempfile = nil, &block)
167
145
  @path = input_path
168
146
  @tempfile = tempfile
169
- @info = {}
170
- reset_queue
171
- end
147
+ @info = MiniMagick::Image::Info.new(@path)
172
148
 
173
- def reset_queue
174
- @command_queued = false
175
- @queue = MiniMagick::CommandBuilder.new('mogrify')
176
- @info.clear
149
+ combine_options(&block) if block
177
150
  end
178
151
 
179
- def run_queue
180
- return nil unless @command_queued
181
- @queue << MiniMagick::Utilities.path(@path)
182
- run(@queue)
183
- reset_queue
152
+ ##
153
+ # Returns raw image data.
154
+ #
155
+ # @return [String] Binary string
156
+ #
157
+ def to_blob
158
+ File.binread(path)
184
159
  end
185
160
 
161
+ ##
186
162
  # Checks to make sure that MiniMagick can read the file and understand it.
187
163
  #
188
164
  # This uses the 'identify' command line utility to check the file. If you
@@ -190,89 +166,110 @@ module MiniMagick
190
166
  # 'identify' command and see if you can figure out what the issue is.
191
167
  #
192
168
  # @return [Boolean]
169
+ #
193
170
  def valid?
194
- run_command('identify', path)
171
+ validate!
195
172
  true
196
173
  rescue MiniMagick::Invalid
197
174
  false
198
175
  end
199
176
 
200
- def info(key)
201
- run_queue if @command_queued
202
-
203
- @info[key]
177
+ ##
178
+ # Runs `identify` on the current image, and raises an error if it doesn't
179
+ # pass.
180
+ #
181
+ # @raise [MiniMagick::Invalid]
182
+ #
183
+ def validate!
184
+ identify
185
+ rescue MiniMagick::Error => error
186
+ raise MiniMagick::Invalid, error.message
204
187
  end
205
188
 
206
- # A rather low-level way to interact with the "identify" command. No nice
207
- # API here, just the crazy stuff you find in ImageMagick. See the examples
208
- # listed!
189
+ ##
190
+ # Returns the image format (e.g. "JPEG", "GIF").
191
+ #
192
+ # @return [String]
193
+ #
194
+ attribute :type, "format"
195
+ ##
196
+ # @return [String]
197
+ #
198
+ attribute :mime_type
199
+ ##
200
+ # @return [Integer]
201
+ #
202
+ attribute :width
203
+ ##
204
+ # @return [Integer]
205
+ #
206
+ attribute :height
207
+ ##
208
+ # @return [Array<Integer>]
209
+ #
210
+ attribute :dimensions
211
+ ##
212
+ # Returns the file size of the image.
213
+ #
214
+ # @return [Integer]
215
+ #
216
+ attribute :size
217
+ ##
218
+ # @return [String]
219
+ #
220
+ attribute :colorspace
221
+ ##
222
+ # @return [Hash]
223
+ #
224
+ attribute :exif
225
+ ##
226
+ # Returns the resolution of the photo. You can optionally specify the
227
+ # units measurement.
228
+ #
229
+ # @example
230
+ # image.resolution("PixelsPerInch") #=> [250, 250]
231
+ # @see http://www.imagemagick.org/script/command-line-options.php#units
232
+ # @return [Array<Integer>]
233
+ #
234
+ attribute :resolution
235
+
236
+ ##
237
+ # Use this method if you want to access raw Identify's format API.
209
238
  #
210
239
  # @example
211
- # image["format"] #=> "TIFF"
212
- # image["height"] #=> 41 (pixels)
213
- # image["width"] #=> 50 (pixels)
214
- # image["colorspace"] #=> "DirectClassRGB"
215
- # image["dimensions"] #=> [50, 41]
216
- # image["size"] #=> 2050 (bits)
217
- # image["original_at"] #=> 2005-02-23 23:17:24 +0000 (Read from Exif data)
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
240
+ # image["%w %h"] #=> "250 450"
241
+ # image["%r"] #=> "DirectClass sRGB"
242
+ #
243
+ # @param value [String]
244
+ # @see http://www.imagemagick.org/script/escape.php
245
+ # @return [String]
246
+ #
224
247
  def [](value)
225
- retrieved = info(value)
226
- return retrieved unless retrieved.nil?
227
-
228
- # Why do I go to the trouble of putting in newlines? Because otherwise
229
- # animated gifs screw everything up
230
- retrieved = case value.to_s
231
- when 'colorspace'
232
- run_command('identify', '-format', '%r\n', path).split("\n")[0].strip
233
- when 'format'
234
- run_command('identify', '-format', '%m\n', path).split("\n")[0]
235
- when 'dimensions', 'width', 'height'
236
- width_height = run_command(
237
- 'identify', '-format', MiniMagick::Utilities.windows? ? '"%w %h\n"' : '%w %h\n', path
238
- ).split("\n")[0].split.map { |v| v.to_i }
239
-
240
- @info[:width] = width_height[0]
241
- @info[:height] = width_height[1]
242
- @info[:dimensions] = width_height
243
- @info[value.to_sym]
244
- when 'size'
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]
248
+ @info[value.to_s]
262
249
  end
250
+ alias info []
263
251
 
264
- # Sends raw commands to imagemagick's `mogrify` command. The image path is
265
- # automatically appended to the command.
252
+ ##
253
+ # Returns layers of the image. For example, JPEGs are 1-layered, but
254
+ # formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
266
255
  #
267
- # Remember, we are always acting on this instance of the Image when messing
268
- # with this.
256
+ # @example
257
+ # image = MiniMagick::Image.new("document.pdf")
258
+ # image.pages.each_with_index do |page, idx|
259
+ # page.write("page#{idx}.pdf")
260
+ # end
261
+ # @return [Array<MiniMagick::Image>]
269
262
  #
270
- # @return [String] Whatever the result from the command line is. May not be
271
- # terribly useful.
272
- def <<(*args)
273
- run_command('mogrify', *args << path)
263
+ def layers
264
+ layers_count = identify.lines.count
265
+ layers_count.times.map do |idx|
266
+ MiniMagick::Image.new("#{path}[#{idx}]")
267
+ end
274
268
  end
269
+ alias pages layers
270
+ alias frames layers
275
271
 
272
+ ##
276
273
  # This is used to change the format of the image. That is, from "tiff to
277
274
  # jpg" or something like that. Once you run it, the instance is pointing to
278
275
  # a new file with a new extension!
@@ -293,169 +290,192 @@ module MiniMagick
293
290
  # @param page [Integer] If this is an animated gif, say which 'page' you
294
291
  # want with an integer. Default 0 will convert only the first page; 'nil'
295
292
  # will convert all pages.
296
- # @return [nil]
293
+ # @yield [MiniMagick::Tool::Convert] It optionally yields the command,
294
+ # if you want to add something.
295
+ # @return [self]
296
+ #
297
297
  def format(format, page = 0)
298
- run_queue if @command_queued
298
+ @info.clear
299
+
300
+ if @tempfile
301
+ new_tempfile = MiniMagick::Utilities.tempfile(".#{format}")
302
+ new_path = new_tempfile.path
303
+ else
304
+ new_path = path.sub(/\.\w+$/, ".#{format}")
305
+ end
299
306
 
300
- c = CommandBuilder.new('mogrify', '-format', format)
301
- yield c if block_given?
302
- c << (page ? "#{path}[#{page}]" : path)
303
- run(c)
307
+ MiniMagick::Tool::Convert.new do |convert|
308
+ convert << (page ? "#{path}[#{page}]" : path)
309
+ yield convert if block_given?
310
+ convert << new_path
311
+ end
304
312
 
305
- old_path = path
313
+ if @tempfile
314
+ @tempfile.unlink
315
+ @tempfile = new_tempfile
316
+ else
317
+ File.delete(path) unless path == new_path
318
+ end
306
319
 
307
- self.path = path.sub(/(\.\w*)?$/, (page ? ".#{format}" : "-0.#{format}"))
320
+ path.replace new_path
308
321
 
309
- File.delete(old_path) if old_path != path
322
+ self
323
+ end
310
324
 
311
- unless File.exist?(path)
312
- fail MiniMagick::Error, "Unable to format to #{format}"
313
- end
325
+ ##
326
+ # You can use multiple commands together using this method. Very easy to
327
+ # use!
328
+ #
329
+ # @example
330
+ # image.combine_options do |c|
331
+ # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
332
+ # c.thumbnail "300x500>"
333
+ # c.background "blue"
334
+ # end
335
+ #
336
+ # @yield [MiniMagick::Tool::Mogrify]
337
+ # @see http://www.imagemagick.org/script/mogrify.php
338
+ # @return [self]
339
+ #
340
+ def combine_options(&block)
341
+ mogrify(&block)
314
342
  end
315
343
 
316
- # Collapse images with sequences to the first frame (i.e. animated gifs) and
317
- # preserve quality
318
- def collapse!
319
- run_command('mogrify', '-quality', '100', "#{path}[0]")
344
+ ##
345
+ # If an unknown method is called then it is sent through the mogrify
346
+ # program.
347
+ #
348
+ # @see http://www.imagemagick.org/script/mogrify.php
349
+ # @return [self]
350
+ #
351
+ def method_missing(name, *args)
352
+ mogrify do |builder|
353
+ if builder.respond_to?(name)
354
+ builder.send(name, *args)
355
+ else
356
+ super
357
+ end
358
+ end
320
359
  end
321
360
 
361
+ ##
322
362
  # Writes the temporary file out to either a file location (by passing in a
323
363
  # String) or by passing in a Stream that you can #write(chunk) to
324
364
  # repeatedly
325
365
  #
326
- # @param output_to [IOStream, String] Some kind of stream object that needs
327
- # to be read or a file path as a String
328
- # @return [IOStream, Boolean] If you pass in a file location [String] then
329
- # you get a success boolean. If its a stream, you get it back.
366
+ # @param output_to [String, Pathname, #read] Some kind of stream object
367
+ # that needs to be read or a file path as a String
368
+ #
330
369
  def write(output_to)
331
- run_queue if @command_queued
332
-
333
- if output_to.kind_of?(String) || output_to.kind_of?(Pathname) || !output_to.respond_to?(:write)
334
- FileUtils.copy_file path, output_to
335
- if MiniMagick.validate_on_write
336
- run_command(
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)
370
+ case output_to
371
+ when String, Pathname
372
+ if layer?
373
+ MiniMagick::Tool::Convert.new do |builder|
374
+ builder << path
375
+ builder << output_to
345
376
  end
377
+ else
378
+ FileUtils.copy_file path, output_to
346
379
  end
347
- output_to
380
+ else
381
+ IO.copy_stream File.open(path, "rb"), output_to
348
382
  end
349
383
  end
350
384
 
351
- # Gives you raw image data back
352
- # @return [String] binary string
353
- def to_blob
354
- run_queue if @command_queued
385
+ ##
386
+ # @example
387
+ # first_image = MiniMagick::Image.open "first.jpg"
388
+ # second_image = MiniMagick::Image.open "second.jpg"
389
+ # result = first_image.composite(second_image) do |c|
390
+ # c.compose "Over" # OverCompositeOp
391
+ # c.geometry "+20+20" # copy second_image onto first_image from (20, 20)
392
+ # end
393
+ # result.write "output.jpg"
394
+ #
395
+ # @see http://www.imagemagick.org/script/composite.php
396
+ #
397
+ def composite(other_image, output_extension = 'jpg', mask = nil)
398
+ output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
399
+
400
+ MiniMagick::Tool::Composite.new do |composite|
401
+ yield composite if block_given?
402
+ composite << other_image.path
403
+ composite << path
404
+ composite << mask.path if mask
405
+ composite << output_tempfile.path
406
+ end
355
407
 
356
- f = File.new path
357
- f.binmode
358
- f.read
359
- ensure
360
- f.close if f
408
+ Image.new(output_tempfile.path, output_tempfile)
361
409
  end
362
410
 
363
- def mime_type
364
- format = self[:format]
365
- 'image/' + format.to_s.downcase
411
+ ##
412
+ # Collapse images with sequences to the first frame (i.e. animated gifs) and
413
+ # preserve quality.
414
+ #
415
+ # @param frame [Integer] The frame to which to collapse to, defaults to `0`.
416
+ # @return [self]
417
+ #
418
+ def collapse!(frame = 0)
419
+ mogrify(frame) { |builder| builder.quality(100) }
366
420
  end
367
421
 
368
- # If an unknown method is called then it is sent through the mogrify
369
- # program.
422
+ ##
423
+ # Destroys the tempfile (created by {.open}) if it exists.
370
424
  #
371
- # @see http://www.imagemagick.org/script/mogrify.php
372
- def method_missing(symbol, *args)
373
- @queue.send(symbol, *args)
374
- @command_queued = true
425
+ def destroy!
426
+ @tempfile.unlink if @tempfile
375
427
  end
376
428
 
377
- # You can use multiple commands together using this method. Very easy to
378
- # use!
429
+ ##
430
+ # Runs `identify` on itself. Accepts an optional block for adding more
431
+ # options to `identify`.
379
432
  #
380
433
  # @example
381
- # image.combine_options do |c|
382
- # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
383
- # c.thumbnail "300x500>"
384
- # c.background background
385
- # end
434
+ # image = MiniMagick::Image.open("image.jpg")
435
+ # image.identify do |b|
436
+ # b.verbose
437
+ # end # runs `identify -verbose image.jpg`
438
+ # @return [String] Output from `identify`
439
+ # @yield [MiniMagick::Tool::Identify]
386
440
  #
387
- # @yieldparam command [CommandBuilder]
388
- def combine_options
389
- if block_given?
390
- yield @queue
391
- @command_queued = true
392
- end
393
- end
394
-
395
- def composite(other_image, output_extension = 'jpg', mask = nil, &block)
396
- run_queue if @command_queued
397
- begin
398
- second_tempfile = Tempfile.new(output_extension)
399
- second_tempfile.binmode
400
- ensure
401
- second_tempfile.close
441
+ def identify
442
+ MiniMagick::Tool::Identify.new do |builder|
443
+ yield builder if block_given?
444
+ builder << path
402
445
  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
446
  end
414
447
 
415
- def run_command(command, *args)
416
- run_queue if @command_queued
417
-
418
- if command == 'identify'
419
- args.unshift '-ping' # -ping "efficiently determine image characteristics."
420
- args.unshift '-quiet' if MiniMagick.mogrify? && !MiniMagick.debug # graphicsmagick has no -quiet option.
448
+ # @private
449
+ def run_command(tool_name, *args)
450
+ MiniMagick::Tool.const_get(tool_name.capitalize).new do |builder|
451
+ args.each do |arg|
452
+ builder << arg
453
+ end
421
454
  end
422
-
423
- run(CommandBuilder.new(command, *args))
424
455
  end
425
456
 
426
- def run(command_builder)
427
- command = command_builder.command
428
-
429
- sub = Subexec.run(command, :timeout => MiniMagick.timeout)
457
+ private
430
458
 
431
- if sub.exitstatus != 0
432
- # Clean up after ourselves in case of an error
433
- destroy!
459
+ def mogrify(page = nil)
460
+ @info.clear
434
461
 
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}"
462
+ MiniMagick::Tool::Mogrify.new do |builder|
463
+ builder.instance_eval do
464
+ def format(*)
465
+ fail NoMethodError,
466
+ "you must call #format on a MiniMagick::Image directly"
467
+ end
442
468
  end
443
- else
444
- sub.output
469
+ yield builder if block_given?
470
+ builder << (page ? "#{path}[#{page}]" : path)
445
471
  end
446
- end
447
472
 
448
- def destroy!
449
- return if @tempfile.nil?
450
- File.unlink(@path) if File.exist?(@path)
451
- @tempfile = nil
473
+ self
452
474
  end
453
475
 
454
- private
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
476
+ def layer?
477
+ path =~ /\[\d+\]$/
459
478
  end
479
+
460
480
  end
461
481
  end