imagery 0.0.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 ImageMagick, ruby and Imagery of
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 imagemagick
22
- # or maybe using macports
23
- sudo port install ImageMagick
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
- Photo = Class.new(Struct.new(:id))
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?('/some/path/here/public/system/photo/1001/thumb.png')
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?('/some/path/here/public/system/photo/1001/original.png')
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
- @avatar ||=
74
- Imagery.new(self).tap do |i|
75
- i.sizes = { :thumb => ["48x48^", "48x48"], :medium => ["120x120"] }
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]) if @avatar_fp
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
- @photo ||=
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::Model
76
+ class S3Photo < Imagery
101
77
  include Imagery::S3
102
- s3_bucket 'my-bucket'
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::Model
83
+ class CloudfrontPhoto < Imagery
107
84
  include Imagery::S3
108
- s3_bucket 'my-bucket'
109
- s3_distribution_domain 'assets.site.com'
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 'http://my.custom.host'
116
- s3_bucket 'my-bucket-name'
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"] = '_access_key_id_'
135
- ENV["AMAZON_SECRET_ACCESS_KEY"] = '_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
- Photo = Class.new(Struct.new(:id))
140
-
141
- class Imagery::Model
117
+ class Imagery
142
118
  include Imagery::S3
143
- s3_bucket 'my-bucket'
119
+ s3_bucket "my-bucket"
144
120
  end
145
121
 
146
- i = Imagery.new(Photo.new(1001))
147
- i.root = '/tmp'
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 |is_real|
177
- user = User.new(:avatar => { tempfile: File.open('avatar.jpg') })
151
+ imagery do |enabled|
152
+ user = User.new(:avatar => { tempfile: File.open("avatar.jpg") })
178
153
  user.save
179
154
 
180
- if is_real
181
- assert File.exist?(user.avatar.file)
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
- module Imagery
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 however, whenever you want
218
- module Imagery
219
- class Model
220
- include Imagery::MogileStore
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
 
@@ -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 = %q{imagery}
8
- s.version = "0.0.6"
9
-
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Cyril David"]
12
- s.date = %q{2010-07-06}
13
- s.description = %q{Uses ImageMagick directly underneath. Nuff said.}
14
- s.email = %q{cyx.ucron@gmail.com}
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
-
@@ -1,20 +1,178 @@
1
- require 'escape'
2
- require 'fileutils'
3
-
4
- module Imagery
5
- VERSION = "0.0.6"
6
-
7
- autoload :Model, "imagery/model"
8
- autoload :Faking, "imagery/faking"
9
- autoload :S3, "imagery/s3"
10
- autoload :Missing, "imagery/missing"
11
- autoload :Test, "imagery/test"
12
-
13
- # Syntactic sugar for Imagery::Model::new
14
- # @yield Imagery::Model
15
- # @see Imagery::Model#initialize for details
16
- def new(*args)
17
- Model.new(*args).tap { |model| yield model if block_given? }
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