imagery 0.0.6 → 0.2.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.
- 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
|