imagery 0.0.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +36 -63
- data/imagery.gemspec +8 -71
- data/lib/imagery.rb +176 -18
- data/lib/imagery/faking.rb +2 -2
- data/lib/imagery/s3.rb +68 -128
- data/lib/imagery/test.rb +5 -5
- data/test/faking.rb +71 -0
- data/test/helper.rb +28 -9
- data/test/imagery.rb +172 -0
- data/test/s3.rb +109 -0
- metadata +22 -91
- data/.document +0 -5
- data/.gitignore +0 -24
- data/Rakefile +0 -56
- data/VERSION +0 -1
- data/lib/imagery/missing.rb +0 -30
- data/lib/imagery/model.rb +0 -217
- data/test/fixtures/lake.jpg +0 -0
- data/test/test_imagery.rb +0 -172
- data/test/test_missing.rb +0 -30
- data/test/test_with_s3.rb +0 -167
data/README.markdown
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
Imagery
|
2
2
|
=======
|
3
3
|
|
4
|
-
(See documentation at [http://labs.sinefunc.com/imagery/doc](http://labs.sinefunc.com/imagery/doc))
|
5
|
-
|
6
4
|
## Image manipulation should be simple. It should be customizable. It should allow for flexibility. Imagery attempts to solve these.
|
7
5
|
|
8
6
|
### Imagery favors:
|
@@ -14,13 +12,13 @@ Imagery
|
|
14
12
|
|
15
13
|
1. Simplicity and Explicitness
|
16
14
|
------------------------------
|
17
|
-
To get started using Imagery you only need
|
15
|
+
To get started using Imagery you only need GraphicsMagick, ruby and Imagery of
|
18
16
|
course.
|
19
17
|
|
20
18
|
# on debian based systems
|
21
|
-
sudo apt-get install
|
22
|
-
# or maybe using
|
23
|
-
|
19
|
+
sudo apt-get install graphicsmagick
|
20
|
+
# or maybe using homebrew
|
21
|
+
brew install graphicsmagick
|
24
22
|
[sudo] gem install imagery
|
25
23
|
|
26
24
|
Then you may proceed using it.
|
@@ -28,28 +26,13 @@ Then you may proceed using it.
|
|
28
26
|
require 'rubygems'
|
29
27
|
require 'imagery'
|
30
28
|
|
31
|
-
|
32
|
-
|
33
|
-
i = Imagery.new(Photo.new(1001))
|
34
|
-
i.root = '/some/path/here'
|
35
|
-
i.sizes = { :thumb => ["48x48^", "48x48"], :large => ["480x320"] }
|
29
|
+
i = Imagery.new(:photo, "1001", thumb: ["48x48^", "48x48"])
|
36
30
|
i.save(File.open('/some/path/to/image.jpg'))
|
37
31
|
|
38
|
-
File.exist?('
|
39
|
-
# => true
|
40
|
-
|
41
|
-
File.exist?('/some/path/here/public/system/photo/1001/large.png')
|
32
|
+
File.exist?('public/photo/1001/thumb.jpg')
|
42
33
|
# => true
|
43
34
|
|
44
|
-
File.exist?('
|
45
|
-
# => true
|
46
|
-
|
47
|
-
# the defaut is to use the .id and the name of the class passed,
|
48
|
-
# but you can specify a different scheme.
|
49
|
-
|
50
|
-
i = Imagery.new(Photo.new(1001), `uuidgen`.strip, "photos")
|
51
|
-
i.root = '/some/path/here'
|
52
|
-
i.file == '/some/path/here/public/system/photos/1c2030a6-6bfa-11df-8997-67a71f1f84c7/original.png'
|
35
|
+
File.exist?('public/photo/1001/original.jpg')
|
53
36
|
# => true
|
54
37
|
|
55
38
|
2. OOP Principles (that we already know)
|
@@ -57,9 +40,6 @@ Then you may proceed using it.
|
|
57
40
|
|
58
41
|
### Ohm example (See [http://ohm.keyvalue.org](http://ohm.keyvalue.org))
|
59
42
|
|
60
|
-
# Imagery will use ROOT_DIR if its available
|
61
|
-
ROOT_DIR = "/u/apps/site/current"
|
62
|
-
|
63
43
|
class User < Ohm::Model
|
64
44
|
include Ohm::Callbacks
|
65
45
|
|
@@ -70,15 +50,14 @@ Then you may proceed using it.
|
|
70
50
|
end
|
71
51
|
|
72
52
|
def avatar
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
53
|
+
Imagery.new :avatar, id,
|
54
|
+
:thumb => ["48x48^", "48x48"],
|
55
|
+
:medium => ["120x120"]
|
77
56
|
end
|
78
57
|
|
79
58
|
protected
|
80
59
|
def write_avatar
|
81
|
-
avatar.save(@avatar_fp[:tempfile])
|
60
|
+
avatar.save(@avatar_fp[:tempfile]) if @avatar_fp
|
82
61
|
end
|
83
62
|
end
|
84
63
|
|
@@ -89,31 +68,30 @@ Then you may proceed using it.
|
|
89
68
|
attribute :height
|
90
69
|
|
91
70
|
def photo
|
92
|
-
|
93
|
-
Imagery.new(self).tap do |i|
|
94
|
-
i.sizes = { :thumb => ["%sx%s" % [width, height]] }
|
95
|
-
end
|
71
|
+
Imagery.new :photo, id, :thumb => ["%sx%s" % [width, height]]
|
96
72
|
end
|
97
73
|
end
|
98
74
|
|
99
75
|
# For cases where we want to use S3 for some and normal filesystem for others
|
100
|
-
class S3Photo < Imagery
|
76
|
+
class S3Photo < Imagery
|
101
77
|
include Imagery::S3
|
102
|
-
|
78
|
+
|
79
|
+
s3_bucket "my-bucket"
|
103
80
|
end
|
104
81
|
|
105
82
|
# then maybe some other files are using cloudfront
|
106
|
-
class CloudfrontPhoto < Imagery
|
83
|
+
class CloudfrontPhoto < Imagery
|
107
84
|
include Imagery::S3
|
108
|
-
|
109
|
-
|
85
|
+
|
86
|
+
s3_bucket "my-bucket"
|
87
|
+
s3_distribution_domain "assets.site.com"
|
110
88
|
end
|
111
89
|
|
112
90
|
# some might be using S3 EU, in which case you can specify the s3_host
|
113
91
|
class CustomS3Host < Imagery::Model
|
114
92
|
include Imagery::S3
|
115
|
-
s3_host
|
116
|
-
s3_bucket
|
93
|
+
s3_host "http://my.custom.host"
|
94
|
+
s3_bucket "my-bucket-name"
|
117
95
|
end
|
118
96
|
|
119
97
|
3. Flexibility and Extensibility
|
@@ -131,21 +109,18 @@ The access credentials are assumed to be stored in
|
|
131
109
|
you can do this by setting it on your .bash_profile / .bashrc or just
|
132
110
|
manually setting them somewhere in your appication
|
133
111
|
|
134
|
-
ENV["AMAZON_ACCESS_KEY_ID"] =
|
135
|
-
ENV["AMAZON_SECRET_ACCESS_KEY"] =
|
112
|
+
ENV["AMAZON_ACCESS_KEY_ID"] = "_access_key_id_"
|
113
|
+
ENV["AMAZON_SECRET_ACCESS_KEY"] = "_secret_access_key_"
|
136
114
|
|
137
115
|
Now you can just start using it:
|
138
116
|
|
139
|
-
|
140
|
-
|
141
|
-
class Imagery::Model
|
117
|
+
class Imagery
|
142
118
|
include Imagery::S3
|
143
|
-
s3_bucket
|
119
|
+
s3_bucket "my-bucket"
|
144
120
|
end
|
145
121
|
|
146
|
-
i = Imagery.new
|
147
|
-
i.
|
148
|
-
i.save(File.open('/some/path/to/image.jpg'))
|
122
|
+
i = Imagery.new :photo, 1001
|
123
|
+
i.save(File.open("/some/path/to/image.jpg"))
|
149
124
|
|
150
125
|
#### Imagery::Faking
|
151
126
|
|
@@ -173,12 +148,12 @@ of testing / faking on an opt-in basis.
|
|
173
148
|
end
|
174
149
|
|
175
150
|
# now when you do some testing... (User assumes the user example above)
|
176
|
-
imagery do |
|
177
|
-
user = User.new(:avatar => { tempfile: File.open(
|
151
|
+
imagery do |enabled|
|
152
|
+
user = User.new(:avatar => { tempfile: File.open("avatar.jpg") })
|
178
153
|
user.save
|
179
154
|
|
180
|
-
if
|
181
|
-
assert File.exist?(user.avatar.
|
155
|
+
if enabled
|
156
|
+
assert File.exist?(user.avatar.root("original.jpg"))
|
182
157
|
end
|
183
158
|
end
|
184
159
|
|
@@ -193,7 +168,7 @@ Imagery doesn't run.
|
|
193
168
|
By making use of standard Ruby idioms, we can easily do lots with it.
|
194
169
|
Exensibility is addressed via Ruby modules for example:
|
195
170
|
|
196
|
-
|
171
|
+
class Imagery
|
197
172
|
module MogileStore
|
198
173
|
def self.included(base)
|
199
174
|
class << base
|
@@ -214,12 +189,10 @@ Exensibility is addressed via Ruby modules for example:
|
|
214
189
|
end
|
215
190
|
end
|
216
191
|
|
217
|
-
# Now just include the module
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
self.mogile_config = { :foo => :bar }
|
222
|
-
end
|
192
|
+
# Now just include the module to use it.
|
193
|
+
class Imagery
|
194
|
+
include Imagery::MogileStore
|
195
|
+
self.mogile_config = { :foo => :bar }
|
223
196
|
end
|
224
197
|
|
225
198
|
|
data/imagery.gemspec
CHANGED
@@ -1,73 +1,10 @@
|
|
1
|
-
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
-
# -*- encoding: utf-8 -*-
|
5
|
-
|
6
1
|
Gem::Specification.new do |s|
|
7
|
-
s.name
|
8
|
-
s.version
|
9
|
-
|
10
|
-
s.
|
11
|
-
s.authors
|
12
|
-
s.
|
13
|
-
s.
|
14
|
-
s.
|
15
|
-
s.extra_rdoc_files = [
|
16
|
-
"LICENSE",
|
17
|
-
"README.markdown"
|
18
|
-
]
|
19
|
-
s.files = [
|
20
|
-
".document",
|
21
|
-
".gitignore",
|
22
|
-
"LICENSE",
|
23
|
-
"README.markdown",
|
24
|
-
"Rakefile",
|
25
|
-
"VERSION",
|
26
|
-
"imagery.gemspec",
|
27
|
-
"lib/imagery.rb",
|
28
|
-
"lib/imagery/faking.rb",
|
29
|
-
"lib/imagery/missing.rb",
|
30
|
-
"lib/imagery/model.rb",
|
31
|
-
"lib/imagery/s3.rb",
|
32
|
-
"lib/imagery/test.rb",
|
33
|
-
"test/fixtures/lake.jpg",
|
34
|
-
"test/helper.rb",
|
35
|
-
"test/test_imagery.rb",
|
36
|
-
"test/test_missing.rb",
|
37
|
-
"test/test_with_s3.rb"
|
38
|
-
]
|
39
|
-
s.homepage = %q{http://github.com/sinefunc/imagery}
|
40
|
-
s.rdoc_options = ["--charset=UTF-8"]
|
41
|
-
s.require_paths = ["lib"]
|
42
|
-
s.rubygems_version = %q{1.3.7}
|
43
|
-
s.summary = %q{Image resizing without all the bloat}
|
44
|
-
s.test_files = [
|
45
|
-
"test/helper.rb",
|
46
|
-
"test/test_imagery.rb",
|
47
|
-
"test/test_missing.rb",
|
48
|
-
"test/test_with_s3.rb"
|
49
|
-
]
|
50
|
-
|
51
|
-
if s.respond_to? :specification_version then
|
52
|
-
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
53
|
-
s.specification_version = 3
|
54
|
-
|
55
|
-
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
56
|
-
s.add_runtime_dependency(%q<escape>, [">= 0"])
|
57
|
-
s.add_development_dependency(%q<contest>, [">= 0"])
|
58
|
-
s.add_development_dependency(%q<aws-s3>, [">= 0"])
|
59
|
-
s.add_development_dependency(%q<mocha>, [">= 0"])
|
60
|
-
else
|
61
|
-
s.add_dependency(%q<escape>, [">= 0"])
|
62
|
-
s.add_dependency(%q<contest>, [">= 0"])
|
63
|
-
s.add_dependency(%q<aws-s3>, [">= 0"])
|
64
|
-
s.add_dependency(%q<mocha>, [">= 0"])
|
65
|
-
end
|
66
|
-
else
|
67
|
-
s.add_dependency(%q<escape>, [">= 0"])
|
68
|
-
s.add_dependency(%q<contest>, [">= 0"])
|
69
|
-
s.add_dependency(%q<aws-s3>, [">= 0"])
|
70
|
-
s.add_dependency(%q<mocha>, [">= 0"])
|
71
|
-
end
|
2
|
+
s.name = "imagery"
|
3
|
+
s.version = "0.2.0"
|
4
|
+
s.summary = "POROS + GraphicsMagick."
|
5
|
+
s.description = "Provide a clean and light interface around GraphicsMagick."
|
6
|
+
s.authors = ["Cyril David"]
|
7
|
+
s.email = ["me@cyrildavid.com"]
|
8
|
+
s.homepage = "http://github.com/cyx/imagery"
|
9
|
+
s.files = ["LICENSE", "README.markdown", "lib/imagery/faking.rb", "lib/imagery/s3.rb", "lib/imagery/test.rb", "lib/imagery.rb", "imagery.gemspec", "test/faking.rb", "test/helper.rb", "test/imagery.rb", "test/s3.rb"]
|
72
10
|
end
|
73
|
-
|
data/lib/imagery.rb
CHANGED
@@ -1,20 +1,178 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
|
4
|
-
|
5
|
-
VERSION = "0.0
|
6
|
-
|
7
|
-
autoload :
|
8
|
-
autoload :Faking,
|
9
|
-
autoload :
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
1
|
+
require "fileutils"
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
class Imagery
|
5
|
+
VERSION = "0.2.0"
|
6
|
+
|
7
|
+
autoload :S3, "imagery/s3"
|
8
|
+
autoload :Faking, "imagery/faking"
|
9
|
+
autoload :Test, "imagery/test"
|
10
|
+
|
11
|
+
# Raised during Imagery#save if the image can't be recognized.
|
12
|
+
InvalidImage = Class.new(StandardError)
|
13
|
+
|
14
|
+
# Acts as a namespace, e.g. `photos`.
|
15
|
+
attr :prefix
|
16
|
+
|
17
|
+
# A unique id for the image.
|
18
|
+
attr :key
|
19
|
+
|
20
|
+
# A hash of name => tuple pairs. The name describes the size, e.g. `small`.
|
21
|
+
#
|
22
|
+
# - The first element if the tuple is the resize geometry.
|
23
|
+
# - The second (optional) element describes the extent.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
#
|
27
|
+
# Imagery.new(:photos, "1001", tiny: ["90x90^", "90x90"])
|
28
|
+
#
|
29
|
+
# @see http://www.graphicsmagick.org/GraphicsMagick.html#details-geometry
|
30
|
+
# @see http://www.graphicsmagick.org/GraphicsMagick.html#details-extent
|
31
|
+
attr :sizes
|
32
|
+
|
33
|
+
# In order to facilitate a plugin architecture, all overridable methods
|
34
|
+
# are placed in the `Core` module. Imagery::S3 demonstrates overriding
|
35
|
+
# in action.
|
36
|
+
module Core
|
37
|
+
def initialize(prefix, key = nil, sizes = {})
|
38
|
+
@prefix = prefix.to_s
|
39
|
+
@key = key.to_s
|
40
|
+
@sizes = sizes
|
41
|
+
@original = :original # Used as the filename for the raw image.
|
42
|
+
@ext = :jpg # We default to jpg for the image format.
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the url for a given size, which defaults to `:original`.
|
46
|
+
#
|
47
|
+
# If the key is nil, a missing path is returned.
|
48
|
+
def url(file = @original)
|
49
|
+
return "/missing/#{prefix}/#{ext(file)}" if key.to_s.empty?
|
50
|
+
|
51
|
+
"/#{prefix}/#{key}/#{ext(file)}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Accepts an `IO` object, typically taken from an input[type=file].
|
55
|
+
#
|
56
|
+
# The second optional `key` argument is used when you want to force
|
57
|
+
# a new resource, useful in conjunction with cloudfront / high cache
|
58
|
+
# scenarios where updating an existing image won't suffice.
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# # Let's say we're in the context of a Sinatra handler,
|
62
|
+
# # and a file was submitted to params[:file]
|
63
|
+
#
|
64
|
+
# post "upload" do
|
65
|
+
# im = Imagery.new(:avatar, current_user.id, thumb: ["20x20"])
|
66
|
+
# im.save(params[:file][:tempfile])
|
67
|
+
#
|
68
|
+
# # At this point we have two files, original.jpg and thumb.jpg
|
69
|
+
#
|
70
|
+
# { original: im.url, thumb: im.url(:thumb) }.to_json
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
def save(io, key = nil)
|
74
|
+
GM.identify(io) or raise(InvalidImage)
|
75
|
+
|
76
|
+
# We delete the existing object iff:
|
77
|
+
# 1. A key was passed
|
78
|
+
# 2. The key passed is different from the existing key.
|
79
|
+
delete if key && key != self.key
|
80
|
+
|
81
|
+
# Now we can assign the new key passed, with the assurance that the
|
82
|
+
# old key has been deleted and won't be used anymore.
|
83
|
+
@key = key.to_s if key
|
84
|
+
|
85
|
+
# Ensure that the path to all images is created.
|
86
|
+
FileUtils.mkdir_p(root)
|
87
|
+
|
88
|
+
# Write the original filename as binary using the `IO` object's data.
|
89
|
+
File.open(root(ext(@original)), "wb") { |file| file.write(io.read) }
|
90
|
+
|
91
|
+
# We resize the original raw image to different sizes which we
|
92
|
+
# defined in the constructor. GraphicsMagick is assumed to exist
|
93
|
+
# within the machine.
|
94
|
+
sizes.each do |size, (resize, extent)|
|
95
|
+
GM.convert root(ext(@original)), root(ext(size)), resize, extent
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# A very simple and destructive method. Deletes the entire folder
|
100
|
+
# for the current prefix/key combination.
|
101
|
+
def delete
|
102
|
+
FileUtils.rm_rf(root)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
include Core
|
106
|
+
|
107
|
+
# Returns the base filename together with the extension,
|
108
|
+
# which defaults to jpg.
|
109
|
+
def ext(file)
|
110
|
+
"#{file}.#{@ext}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def root(*args)
|
114
|
+
self.class.root(prefix, key, *args)
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.root(*args)
|
118
|
+
File.join(@root, *args)
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.root=(path)
|
122
|
+
@root = path
|
123
|
+
end
|
124
|
+
self.root = File.join(Dir.pwd, "public")
|
125
|
+
|
126
|
+
module GM
|
127
|
+
# -size tells GM to only read from a given dimension.
|
128
|
+
# -resize is the target dimension, and understands geometry strings.
|
129
|
+
# -quality we force it to 80, which is very reasonable and practical.
|
130
|
+
CONVERT = "gm convert -size '%s' '%s' -resize '%s' %s -quality 80 '%s'"
|
131
|
+
|
132
|
+
# 2 is the file descriptor for stderr, which `gm identify`
|
133
|
+
# happily chucks out information to, regardless if the image
|
134
|
+
# was identified or not.
|
135
|
+
#
|
136
|
+
# We utilize the fact that gm identify exits with a status of 1 if
|
137
|
+
# it fails to identify the image.
|
138
|
+
#
|
139
|
+
# @see for an explanation of file descriptions and redirection.
|
140
|
+
# http://stackoverflow.com/questions/818255/in-the-bash-shell-what-is-21
|
141
|
+
IDENTIFY = "gm identify '%s' 2> /dev/null"
|
142
|
+
|
143
|
+
def self.convert(src, dst, resize, extent = nil)
|
144
|
+
system(sprintf(CONVERT, dim(resize), src, resize, extent(extent), dst))
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.identify(io)
|
148
|
+
file = Tempfile.new("imagery")
|
149
|
+
file.write(io.read)
|
150
|
+
file.close
|
151
|
+
|
152
|
+
`gm identify #{io.path} 2> /dev/null`
|
153
|
+
|
154
|
+
return $?.success?
|
155
|
+
ensure
|
156
|
+
# Very important, else `io.read` will return "".
|
157
|
+
io.rewind
|
158
|
+
|
159
|
+
# Tempfile quickly runs out of names, so best to avoid that.
|
160
|
+
file.unlink
|
161
|
+
end
|
162
|
+
|
163
|
+
# Return the cleaned dimension representation minus the
|
164
|
+
# geometry directives.
|
165
|
+
def self.dim(dim)
|
166
|
+
dim.gsub(/\^><!/, "")
|
167
|
+
end
|
168
|
+
|
169
|
+
# Cropping and all that nice presentation kung-fu.
|
170
|
+
#
|
171
|
+
# @see http://www.graphicsmagick.org/GraphicsMagick.html#details-extent
|
172
|
+
def self.extent(dim)
|
173
|
+
if dim
|
174
|
+
"-background black -compose Copy -gravity center -extent '#{dim}'"
|
175
|
+
end
|
176
|
+
end
|
18
177
|
end
|
19
|
-
module_function :new
|
20
178
|
end
|