httpthumbnailer-client 1.2.0 → 1.3.0

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.
@@ -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
+