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.
- checksums.yaml +15 -0
- data/Gemfile +5 -5
- data/Gemfile.lock +58 -30
- data/README.md +33 -5
- data/Rakefile +5 -1
- data/VERSION +1 -1
- data/bin/httpthumbnailer-client +9 -27
- data/lib/httpthumbnailer-client.rb +9 -45
- data/lib/httpthumbnailer-client/thumbnail_spec.rb +178 -0
- data/lib/httpthumbnailer-client/uri_builder.rb +43 -0
- metadata +35 -62
- data/.rspec +0 -1
- data/features/step_definitions/httpthumbnailer-client_steps.rb +0 -0
- data/features/support/env.rb +0 -13
- data/httpthumbnailer-client.gemspec +0 -95
- data/spec/httpthumbnailer-client_spec.rb +0 -18
- data/spec/identify_spec.rb +0 -44
- data/spec/spec_helper.rb +0 -83
- data/spec/support/test-large.jpg +0 -0
- data/spec/support/test.jpg +0 -0
- data/spec/support/test.txt +0 -1
- data/spec/thumbnail_spec.rb +0 -102
- data/spec/thumbnails_spec.rb +0 -139
- data/spec/uri_builder_spec.rb +0 -16
checksums.yaml
ADDED
@@ -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
|
14
|
-
gem "jeweler", "~> 1.8.
|
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
|
data/Gemfile.lock
CHANGED
@@ -1,53 +1,82 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ../httpthumbnailer
|
3
3
|
specs:
|
4
|
-
httpthumbnailer (1.
|
4
|
+
httpthumbnailer (1.3.0)
|
5
5
|
rmagick (~> 2)
|
6
|
-
unicorn-cuba-base (~> 1.
|
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.
|
11
|
+
unicorn-cuba-base (1.6.0)
|
12
12
|
cli (~> 1.3)
|
13
13
|
cuba (~> 3.0)
|
14
|
-
facter (~> 1.6.
|
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
|
-
|
22
|
+
addressable (2.3.8)
|
23
|
+
builder (3.2.2)
|
23
24
|
cli (1.3.1)
|
24
|
-
cuba (3.
|
25
|
+
cuba (3.4.0)
|
25
26
|
rack
|
26
|
-
cucumber (1.
|
27
|
+
cucumber (1.3.17)
|
27
28
|
builder (>= 2.1.2)
|
28
|
-
diff-lcs (>= 1.1.
|
29
|
-
gherkin (~> 2.
|
30
|
-
|
31
|
-
|
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
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
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.
|
45
|
-
|
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
|
-
|
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.
|
50
|
-
rdoc (3.
|
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.
|
61
|
-
|
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.
|
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
|
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
|
-
|
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{
|
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.
|
1
|
+
1.3.0
|
data/bin/httpthumbnailer-client
CHANGED
@@ -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
|
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 =
|
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
|
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
|
-
|
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
|
-
|
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
|
+
|