httpthumbnailer-client 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MDY0YzBlNDVlNjUwN2M0MDRhY2E3Mjg1ZjEzMTY2YzFlYjdlNjE1OA==
5
+ data.tar.gz: !binary |-
6
+ ZTc3ODZjY2QxNzBmYzgxZjg3ZjM1ZWZlNDEwODk3M2JlMDFlMzgwMA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OTA5ZDZmZGQ0MjU0NmYzZDdhZjMzMmE3MGYzMzA3Zjg3MDNjNTNjYjIyN2Mx
10
+ NWVkM2ExMGE1MDQ4ZmNhZTIzM2RjYzkyNmY4NWYzMjhkYmFmMmE2ZDc5NjE0
11
+ MGM3ZDljNjgxNjkwMTI2ZDZkZGM5ODQ2YzcyM2VhMGZmMmMxMDI=
12
+ data.tar.gz: !binary |-
13
+ YmIyZDFkMDIwOTc0M2JhODBiNTNhYzAxZmNhNmM1NWIwNzg4MmJmYjE4YmMz
14
+ YWRhM2NmNjM1NTMzZTRhYWRkNjAwYTg4ZmFlMzE4ZjU4OTVlZDc4NmMxZGY0
15
+ ZWMyMTNiODhiZGViMzg1NDgzMmFhN2RlNDQ4OWYxMGM4NDQzYzE=
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "http://rubygems.org"
2
2
  ruby "1.9.3"
3
3
 
4
- gem "httpclient", ">= 2.3"
4
+ gem "httpclient", "~> 2.0", ">= 2.3"
5
5
  gem "cli", "~> 1.3"
6
6
  gem "multipart-parser", "~> 0.1.1"
7
7
 
@@ -10,11 +10,11 @@ gem "multipart-parser", "~> 0.1.1"
10
10
  group :development do
11
11
  gem "rspec", "~> 2.13"
12
12
  gem "rspec-mocks", "~> 2.13"
13
- gem "cucumber", ">= 0"
14
- gem "jeweler", "~> 1.8.4"
13
+ gem 'cucumber', '~> 1.3'
14
+ gem "jeweler", "~> 1.8", ">= 1.8.8"
15
15
  gem "rdoc", "~> 3.9"
16
16
  gem "daemon", "~> 1"
17
17
  gem "rmagick", "~> 2"
18
- gem "httpthumbnailer", path: '../httpthumbnailer'
19
- gem "unicorn-cuba-base", path: '../unicorn-cuba-base'
18
+ gem "httpthumbnailer", "~> 1", path: '../httpthumbnailer'
19
+ gem "unicorn-cuba-base", "~> 1", path: '../unicorn-cuba-base'
20
20
  end
@@ -1,53 +1,82 @@
1
1
  PATH
2
2
  remote: ../httpthumbnailer
3
3
  specs:
4
- httpthumbnailer (1.2.0)
4
+ httpthumbnailer (1.3.0)
5
5
  rmagick (~> 2)
6
- unicorn-cuba-base (~> 1.2.0)
6
+ unicorn-cuba-base (~> 1.6)
7
7
 
8
8
  PATH
9
9
  remote: ../unicorn-cuba-base
10
10
  specs:
11
- unicorn-cuba-base (1.2.0)
11
+ unicorn-cuba-base (1.6.0)
12
12
  cli (~> 1.3)
13
13
  cuba (~> 3.0)
14
- facter (~> 1.6.11)
14
+ facter (~> 1.6, >= 1.6.18)
15
15
  raindrops (~> 0.11)
16
16
  ruby-ip (~> 0.9)
17
- unicorn (>= 4.6.2)
17
+ unicorn (~> 4.6, >= 4.6.2)
18
18
 
19
19
  GEM
20
20
  remote: http://rubygems.org/
21
21
  specs:
22
- builder (3.0.0)
22
+ addressable (2.3.8)
23
+ builder (3.2.2)
23
24
  cli (1.3.1)
24
- cuba (3.1.1)
25
+ cuba (3.4.0)
25
26
  rack
26
- cucumber (1.1.3)
27
+ cucumber (1.3.17)
27
28
  builder (>= 2.1.2)
28
- diff-lcs (>= 1.1.2)
29
- gherkin (~> 2.6.7)
30
- json (>= 1.4.6)
31
- term-ansicolor (>= 1.0.6)
29
+ diff-lcs (>= 1.1.3)
30
+ gherkin (~> 2.12)
31
+ multi_json (>= 1.7.5, < 2.0)
32
+ multi_test (>= 0.1.1)
32
33
  daemon (1.1.0)
33
34
  diff-lcs (1.1.3)
34
- facter (1.6.18)
35
- gherkin (2.6.7)
36
- json (>= 1.4.6)
37
- git (1.2.5)
35
+ facter (1.7.6)
36
+ faraday (0.8.9)
37
+ multipart-post (~> 1.2.0)
38
+ gherkin (2.12.2)
39
+ multi_json (~> 1.3)
40
+ git (1.2.9.1)
41
+ github_api (0.10.1)
42
+ addressable
43
+ faraday (~> 0.8.1)
44
+ hashie (>= 1.2)
45
+ multi_json (~> 1.4)
46
+ nokogiri (~> 1.5.2)
47
+ oauth2
48
+ hashie (3.4.2)
49
+ highline (1.7.2)
38
50
  httpclient (2.3.3)
39
- jeweler (1.8.4)
51
+ jeweler (1.8.8)
52
+ builder
40
53
  bundler (~> 1.0)
41
54
  git (>= 1.2.5)
55
+ github_api (= 0.10.1)
56
+ highline (>= 1.6.15)
57
+ nokogiri (= 1.5.10)
42
58
  rake
43
59
  rdoc
44
- json (1.6.1)
45
- kgio (2.9.2)
60
+ json (1.8.3)
61
+ jwt (1.5.0)
62
+ kgio (2.9.3)
63
+ multi_json (1.10.1)
64
+ multi_test (0.1.1)
65
+ multi_xml (0.5.5)
46
66
  multipart-parser (0.1.1)
47
- rack (1.5.2)
67
+ multipart-post (1.2.0)
68
+ nokogiri (1.5.10)
69
+ oauth2 (1.0.0)
70
+ faraday (>= 0.8, < 0.10)
71
+ jwt (~> 1.0)
72
+ multi_json (~> 1.3)
73
+ multi_xml (~> 0.5)
74
+ rack (~> 1.2)
75
+ rack (1.6.1)
48
76
  raindrops (0.13.0)
49
- rake (10.0.4)
50
- rdoc (3.9.4)
77
+ rake (10.4.2)
78
+ rdoc (3.12.2)
79
+ json (~> 1.4)
51
80
  rmagick (2.13.2)
52
81
  rspec (2.13.0)
53
82
  rspec-core (~> 2.13.0)
@@ -57,9 +86,8 @@ GEM
57
86
  rspec-expectations (2.13.0)
58
87
  diff-lcs (>= 1.1.3, < 2.0)
59
88
  rspec-mocks (2.13.1)
60
- ruby-ip (0.9.1)
61
- term-ansicolor (1.0.7)
62
- unicorn (4.8.2)
89
+ ruby-ip (0.9.3)
90
+ unicorn (4.9.0)
63
91
  kgio (~> 2.6)
64
92
  rack
65
93
  raindrops (~> 0.7)
@@ -69,14 +97,14 @@ PLATFORMS
69
97
 
70
98
  DEPENDENCIES
71
99
  cli (~> 1.3)
72
- cucumber
100
+ cucumber (~> 1.3)
73
101
  daemon (~> 1)
74
- httpclient (>= 2.3)
75
- httpthumbnailer!
76
- jeweler (~> 1.8.4)
102
+ httpclient (~> 2.0, >= 2.3)
103
+ httpthumbnailer (~> 1)!
104
+ jeweler (~> 1.8, >= 1.8.8)
77
105
  multipart-parser (~> 0.1.1)
78
106
  rdoc (~> 3.9)
79
107
  rmagick (~> 2)
80
108
  rspec (~> 2.13)
81
109
  rspec-mocks (~> 2.13)
82
- unicorn-cuba-base!
110
+ unicorn-cuba-base (~> 1)!
data/README.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # HTTP Thubnailer Client
2
2
 
3
- Ruby client to [httpthumbnailer](http://github.com/jpastuszek/httpthumbnailer) image scaling and conversion HTTP API server.
3
+ Ruby client to [httpthumbnailer](http://github.com/jpastuszek/httpthumbnailer) image thumbnailing, editing and format conversion HTTP API server.
4
+
5
+ ## Changelog
6
+
7
+ ### 1.3.0
8
+ * added edits support
9
+ * better spec parsing and error handling
10
+ * using single thumbnail API if only one thumbnail spec defined
4
11
 
5
12
  ## Installing
6
13
 
@@ -30,7 +37,7 @@ thumbnail.data # => 60x30 thumbnail JPEG data String
30
37
 
31
38
  # generate set of thumbnails from image data (multipart API)
32
39
  thumbnails = HTTPThumbnailerClient.new('http://localhost:3100').thumbnail(data) do
33
- thumbnail 'crop', 60, 30, 'jpeg'
40
+ thumbnail 'crop', 60, 30, 'jpeg'
34
41
  thumbnail 'crop', 80, 80, 'png'
35
42
  thumbnail 'pad', 40, 40, 'png'
36
43
  end
@@ -65,9 +72,28 @@ id = HTTPThumbnailerClient.new('http://localhost:3100').with_headers('XID' => '1
65
72
  id.mime_type # => 'image/jpeg'
66
73
  id.width # => 800
67
74
  id.height # => 600
75
+
76
+ # generate single thumbnail from image data with additional edits
77
+ thumbnail = HTTPThumbnailerClient.new('http://localhost:3100').thumbnail(data, 'crop', 60, 30, 'jpeg') do
78
+ edit 'rotate', 90
79
+ edit 'pixelate', 0.3, 0.2, 0.4, 0.4
80
+ end
81
+
82
+ # generate set of thumbnails from image data with additional edits
83
+ thumbnails = HTTPThumbnailerClient.new('http://localhost:3100').thumbnail(data) do
84
+ thumbnail 'crop', 60, 30, 'jpeg' do
85
+ edit 'rotate', 90
86
+ end
87
+ thumbnail 'crop', 80, 80, 'png' do
88
+ edit 'rotate', 270
89
+ edit 'pixelate', 0.3, 0.2, 0.4, 0.4
90
+ end
91
+ thumbnail 'pad', 40, 40, 'png'
92
+ end
68
93
  ```
69
94
 
70
95
  For more details see RSpec for [single thumbnail API](http://github.com/jpastuszek/httpthumbnailer-client/blob/master/spec/thumbnail_spec.rb) and [multipart API](http://github.com/jpastuszek/httpthumbnailer-client/blob/master/spec/thumbnails_spec.rb).
96
+ Note that when providing only one thumbnail specification using multipart API style the client will actually use single thumbnail API instead.
71
97
 
72
98
  ### CLI tool
73
99
 
@@ -85,10 +111,13 @@ cat image.jpg | httpthumbnailer-client -t crop,100,200,png > thumbnail.png
85
111
 
86
112
  # generate multiple thumbnails
87
113
  cat image.jpg | httpthumbnailer-client -t crop,100,200,jpeg,quality:100 -t pad,200,200,png thumbnail1.jpg thumbnail2.png
114
+
115
+ # generate thumbnail with edits
116
+ cat image.jpg | httpthumbnailer-client -t 'fit,280,280,png!pixelate,0.3,0.2,0.4,0.4!rectangle,0.04,0.8,0.92,0.17,color:blue' > thumbnail.png
88
117
  ```
89
118
 
90
119
  ## Contributing to HTTP Thubnailer Client
91
-
120
+
92
121
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
93
122
  * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
94
123
  * Fork the project
@@ -99,6 +128,5 @@ cat image.jpg | httpthumbnailer-client -t crop,100,200,jpeg,quality:100 -t pad,2
99
128
 
100
129
  ## Copyright
101
130
 
102
- Copyright (c) 2013 Jakub Pastuszek. See LICENSE.txt for
103
- further details.
131
+ Copyright (c) 2013 - 2015 Jakub Pastuszek. See LICENSE.txt for further details.
104
132
 
data/Rakefile CHANGED
@@ -18,9 +18,13 @@ Jeweler::Tasks.new do |gem|
18
18
  gem.homepage = "http://github.com/jpastuszek/httpthumbnailer-client"
19
19
  gem.license = "MIT"
20
20
  gem.summary = %Q{API client for httpthumbniler server}
21
- gem.description = %Q{Thumbnails images using httpthumbniler server}
21
+ gem.description = %Q{Ruby client for HTTP API server for image thumbnailing, editing and format conversion.}
22
22
  gem.email = "jpastuszek@gmail.com"
23
23
  gem.authors = ["Jakub Pastuszek"]
24
+ gem.files.exclude "features/**/*"
25
+ gem.files.exclude "spec/**/*"
26
+ gem.files.exclude "*.gemspec"
27
+ gem.files.exclude ".rspec"
24
28
  # dependencies defined in Gemfile
25
29
  end
26
30
  Jeweler::RubygemsDotOrgTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 1.3.0
@@ -5,20 +5,6 @@ require 'pathname'
5
5
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
6
  require 'httpthumbnailer-client'
7
7
 
8
- def parse_spec(spec)
9
- method, width, height, format, *options = *spec.split(',')
10
- fail "missing thumbnail specification argument in: #{spec}" unless method and width and height and format
11
-
12
- opts = {}
13
- options.each do |option|
14
- key, value = option.split(':')
15
- fail "missing option key or value in specifiaction: #{spec}" unless key and value
16
- opts[key] = value
17
- end
18
-
19
- [method, width, height, format, opts]
20
- end
21
-
22
8
  settings = CLI.new do
23
9
  description 'Client to httpthumbnailer image scaling and conversion HTTP API server'
24
10
  stdin :image_data,
@@ -43,7 +29,9 @@ settings = CLI.new do
43
29
  end.parse! do |settings|
44
30
  STDERR.puts "Warning: there is more thumbnail specifications given than output file names; some thumbnails will not be saved" if settings.thumbnail.length > settings.output_file_name.length
45
31
  fail 'only one thumbnail can be written to STDOUT' if settings.output_file_name.count('-') > 1
46
- settings.thumbnail = settings.thumbnail.map{|spec| parse_spec(spec)}
32
+ settings.thumbnail.map! do |spec|
33
+ HTTPThumbnailerClient::ThumbnailSpec.from_string(spec)
34
+ end
47
35
  end
48
36
 
49
37
  thumbnailer = HTTPThumbnailerClient.new("http://#{settings.host}:#{settings.port}")
@@ -56,20 +44,14 @@ if settings.thumbnail.empty?
56
44
  exit
57
45
  end
58
46
 
59
- thumbnails = if settings.thumbnail.length == 1
60
- [thumbnailer.thumbnail(settings.stdin.read, *settings.thumbnail.first)]
61
- else
62
- thumbnailer.thumbnail(settings.stdin.read) do
63
- settings.thumbnail.each do |spec|
64
- thumbnail *spec
65
- end
66
- end
67
- end
47
+ thumbnails = thumbnailer.thumbnail(settings.stdin.read, *settings.thumbnail)
68
48
 
69
- thumbnails.zip(settings.output_file_name).select do |thumbnail, out_file|
70
- out_file
49
+ thumbnails.zip(settings.output_file_name).select do |thumbnail, out_file|
50
+ out_file
71
51
  end.each do |thumbnail, out_file|
72
- if out_file == '-'
52
+ if thumbnail.kind_of? HTTPThumbnailerClient::HTTPThumbnailerClientError
53
+ STDERR.puts "Error: thumbnailing for '#{out_file}' failed: #{thumbnail}"
54
+ elsif out_file == '-'
73
55
  STDOUT.write thumbnail.data
74
56
  else
75
57
  File.open(out_file, 'w') do |file|
@@ -2,6 +2,7 @@ require 'httpclient'
2
2
  require 'ostruct'
3
3
  require 'json'
4
4
  require 'multipart_parser/reader'
5
+ require 'httpthumbnailer-client/uri_builder'
5
6
 
6
7
  class HTTPThumbnailerClient
7
8
  HTTPThumbnailerClientError = Class.new(ArgumentError)
@@ -56,48 +57,6 @@ class HTTPThumbnailerClient
56
57
  UnknownResponseType = Class.new HTTPThumbnailerClientError
57
58
  InvalidMultipartResponseError = Class.new HTTPThumbnailerClientError
58
59
 
59
- class URIBuilder
60
- def initialize(service_uri, &block)
61
- @specs = []
62
- @service_uri = service_uri
63
- instance_eval &block if block
64
- end
65
-
66
- def get
67
- "#{@service_uri}/#{@specs.join('/')}"
68
- end
69
-
70
- def self.thumbnail(*spec)
71
- self.new('/thumbnail').thumbnail(*spec).get
72
- end
73
-
74
- def self.thumbnails(&block)
75
- self.new('/thumbnails', &block).get
76
- end
77
-
78
- def thumbnail(method, width, height, format = 'jpeg', options = {})
79
- width = width.to_s
80
- height = height.to_s
81
-
82
- args = []
83
- args << method.to_s
84
- width !~ /^([0-9]+|input)$/ and raise InvalidThumbnailSpecificationError.new("bad dimension value: #{width}")
85
- args << width
86
- height !~ /^([0-9]+|input)$/ and raise InvalidThumbnailSpecificationError.new("bad dimension value: #{height}")
87
- args << height
88
- args << format.to_s
89
-
90
- options.keys.sort{|a, b| a.to_s <=> b.to_s}.each do |key|
91
- raise InvalidThumbnailSpecificationError.new("empty option key for value '#{options[key]}'") if key.nil? || key.to_s.empty?
92
- raise InvalidThumbnailSpecificationError.new("missing option value for key '#{key}'") if options[key].nil? || options[key].to_s.empty?
93
- args << "#{key}:#{options[key]}"
94
- end
95
-
96
- @specs << args.join(',')
97
- self
98
- end
99
- end
100
-
101
60
  class Thumbnail
102
61
  def initialize(mime_type, width, height, data)
103
62
  @mime_type = mime_type
@@ -154,10 +113,14 @@ class HTTPThumbnailerClient
154
113
  attr_reader :keep_alive
155
114
 
156
115
  def thumbnail(data, *spec, &block)
157
- uri = if block
116
+ return_array = true
117
+ uri = if spec.empty?
158
118
  URIBuilder.thumbnails(&block)
119
+ elsif spec.all?{|s| s.kind_of? ThumbnailSpec}
120
+ URIBuilder.specs(*spec)
159
121
  else
160
- URIBuilder.thumbnail(*spec)
122
+ return_array = false # for .thumbnail(data, 'crop', 60, 30, 'jpeg') kind of usage
123
+ URIBuilder.thumbnail(*spec, &block)
161
124
  end
162
125
 
163
126
  response = @client.request('PUT', "#{@server_url}#{uri}", nil, data, {'Content-Type' => 'image/autodetect'}.merge(@headers))
@@ -169,7 +132,8 @@ class HTTPThumbnailerClient
169
132
  when 'text/plain'
170
133
  raise error_for_status(response.status, response.body)
171
134
  when /^image\//
172
- Thumbnail.new(content_type, response.headers['X-Image-Width'], response.headers['X-Image-Height'], response.body)
135
+ thumb = Thumbnail.new(content_type, response.headers['X-Image-Width'], response.headers['X-Image-Height'], response.body)
136
+ return_array ? [thumb] : thumb
173
137
  when /^multipart\/mixed/
174
138
  parts = []
175
139
  parser = MultipartParser::Reader.new(MultipartParser::Reader.extract_boundary_value(content_type))
@@ -0,0 +1,178 @@
1
+ class HTTPThumbnailerClient
2
+ #TODO: support for escaping of ! and ,
3
+ class ThumbnailSpec
4
+ class Builder
5
+ def initialize(method, width, height, format = 'jpeg', options = {}, &block)
6
+ @spec = ThumbnailSpec.new(method, width.to_s, height.to_s, format, options)
7
+ instance_eval(&block) if block
8
+ end
9
+
10
+ def edit(name, *args)
11
+ edit_options, args = *args.partition{|e| e.kind_of? Hash}
12
+ edit_options = edit_options.each.with_object({}) do |opt, hash|
13
+ hash.merge! opt
14
+ end
15
+
16
+ edit_spec ThumbnailSpec::EditSpec.new(name, args, edit_options)
17
+ self
18
+ end
19
+
20
+ def edit_spec(spec)
21
+ @spec.edits << spec
22
+ self
23
+ end
24
+
25
+ attr_reader :spec
26
+
27
+ def to_s
28
+ @spec.to_s
29
+ end
30
+ end
31
+
32
+ class InvalidFormatError < ArgumentError
33
+ def for_edit(name)
34
+ exception "#{message} for edit '#{name}'"
35
+ end
36
+
37
+ def in_spec(spec)
38
+ exception "#{message} in spec '#{spec}'"
39
+ end
40
+ end
41
+
42
+ class MissingArgumentError < InvalidFormatError
43
+ def initialize(argument)
44
+ super "missing #{argument} argument"
45
+ end
46
+ end
47
+
48
+ class InvalidArgumentValueError < InvalidFormatError
49
+ def initialize(name, value, reason)
50
+ super "#{name} value '#{value}' is not #{reason}"
51
+ end
52
+ end
53
+
54
+ class MissingOptionKeyValuePairError < InvalidFormatError
55
+ def initialize(index)
56
+ super "missing key-value pair on position #{index + 1}"
57
+ end
58
+ end
59
+
60
+ class MissingOptionKeyNameError < InvalidFormatError
61
+ def initialize(value)
62
+ super "missing option key name for value '#{value}'"
63
+ end
64
+ end
65
+
66
+ class MissingOptionKeyValueError < InvalidFormatError
67
+ def initialize(key)
68
+ super "missing option value for key '#{key}'"
69
+ end
70
+ end
71
+
72
+ class EditSpec
73
+ attr_reader :name, :args, :options
74
+
75
+ def self.from_string(string)
76
+ args = HTTPThumbnailerClient::ThumbnailSpec.split_args(string)
77
+ args, options = HTTPThumbnailerClient::ThumbnailSpec.partition_args_options(args)
78
+ name = args.shift
79
+
80
+ begin
81
+ options = HTTPThumbnailerClient::ThumbnailSpec.parse_options(options)
82
+ rescue InvalidFormatError => error
83
+ raise error.for_edit(name)
84
+ end
85
+ new(name, args, options)
86
+ end
87
+
88
+ def initialize(name, args, options = {})
89
+ name.nil? or name.empty? and raise MissingArgumentError, 'edit name'
90
+
91
+ @name = name
92
+ @args = args
93
+ @options = options
94
+ end
95
+
96
+ def to_s
97
+ begin
98
+ [@name, *@args, *HTTPThumbnailerClient::ThumbnailSpec.options_to_s(@options)].join(',')
99
+ rescue InvalidFormatError => error
100
+ raise error.for_edit(name)
101
+ end
102
+ end
103
+ end
104
+
105
+ attr_reader :method, :width, :height, :format, :options, :edits
106
+
107
+ def self.from_string(string)
108
+ edits = split_edits(string)
109
+ spec = edits.shift
110
+ args = split_args(spec)
111
+ args, options = partition_args_options(args)
112
+ method, width, height, format = *args.shift(4) # ignore extra args
113
+
114
+ options = parse_options(options)
115
+ edits = edits.map{|e| EditSpec.from_string(e)}
116
+
117
+ new(method, width, height, format, options, edits)
118
+ rescue InvalidFormatError => error
119
+ raise error.in_spec(string)
120
+ end
121
+
122
+ def initialize(method, width, height, format, options = {}, edits = [])
123
+ method.nil? or method.empty? and raise MissingArgumentError, 'method'
124
+ width.nil? or width.empty? and raise MissingArgumentError, 'width'
125
+ height.nil? or height.empty? and raise MissingArgumentError, 'height'
126
+ format.nil? or format.empty? and raise MissingArgumentError, 'format'
127
+
128
+ width !~ /^([0-9]+|input)$/ and raise InvalidArgumentValueError.new('width', width, "an integer or 'input'")
129
+ height !~ /^([0-9]+|input)$/ and raise InvalidArgumentValueError.new('height', height, "an integer or 'input'")
130
+ @method = method
131
+ @width = width
132
+ @height = height
133
+ @format = format
134
+ @options = options
135
+ @edits = edits
136
+ end
137
+
138
+ def to_s
139
+ [[@method, @width, @height, @format, *self.class.options_to_s(@options)].join(','), *@edits.map(&:to_s)].join('!')
140
+ end
141
+
142
+ def self.split_edits(string)
143
+ string.split('!')
144
+ end
145
+
146
+ def self.split_args(string)
147
+ string.split(',')
148
+ end
149
+
150
+ def self.partition_args_options(args)
151
+ options = args.drop_while{|a| not a.include?(':')}
152
+ args = args.take_while{|a| not a.include?(':')}
153
+ [args, options]
154
+ end
155
+
156
+ def self.parse_options(options)
157
+ Hash[options.map.with_index do |pair, index|
158
+ pair.empty? and raise MissingOptionKeyValuePairError, index
159
+ pair.split(':', 2)
160
+ end].tap do |map|
161
+ map.each do |key, value|
162
+ key.nil? or key.empty? and raise MissingOptionKeyNameError, value
163
+ value.nil? or value.empty? and raise MissingOptionKeyValueError, key
164
+ end
165
+ end
166
+ end
167
+
168
+ def self.options_to_s(options)
169
+ options.sort_by{|k,v| k}.map do |key, value|
170
+ raise MissingOptionKeyNameError, value if key.nil? or key.to_s.empty?
171
+ raise MissingOptionKeyValueError, key if value.nil? or value.to_s.empty?
172
+ key = key.to_s.gsub('_', '-') if key.kind_of? Symbol
173
+ "#{key}:#{value}"
174
+ end
175
+ end
176
+ end
177
+ end
178
+