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