meme_captain 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog CHANGED
@@ -1,3 +1,17 @@
1
+ 0.1.0 2012-02-07
2
+
3
+ Redesign web interface.
4
+
5
+ Avoid repeated failures fetching the same source image from the web
6
+ interface / API.
7
+
8
+ ruby interface now supports adding any number of text strings to
9
+ an image.
10
+
11
+ Add custom text positioning and sizing to web interface, API and
12
+ ruby interface. Web interface has text positioning widget using
13
+ fabric.js and canvas.
14
+
1
15
  0.0.9 2012-01-19
2
16
 
3
17
  Design and markup improvements on the site.
data/README.md CHANGED
@@ -1,21 +1,48 @@
1
- Ruby gem to create meme images (images with text added at the top and bottom).
1
+ Ruby gem to create meme images (images with text added).
2
2
 
3
3
  Runs locally and has no web dependencies.
4
4
 
5
5
  Works with animated gifs.
6
6
 
7
+ Create a simple top and bottom text meme:
8
+
7
9
  ```ruby
8
10
  require 'open-uri'
9
11
 
10
12
  require 'meme_captain'
11
13
 
12
14
  open('http://memecaptain.com/troll_face.jpg', 'rb') do |f|
13
- i = MemeCaptain.meme(f, 'test', '1 2 3')
15
+ i = MemeCaptain.meme_top_bottom(f, 'test', '1 2 3')
16
+ i.display
17
+ i.write('out.jpg')
18
+ end
19
+ ```
20
+
21
+ Advanced usage with text sizing and positioning and RMagick attributes:
22
+
23
+ ```ruby
24
+ require 'open-uri'
25
+
26
+ require 'meme_captain'
27
+
28
+ open('http://memecaptain.com/cool_story_bro.jpg', 'rb') do |f|
29
+ i = MemeCaptain.meme(f, [
30
+ MemeCaptain::TextPos.new('the quick brown fox', 0.70, 0.1, 0.25, 0.5,
31
+ :fill => 'green'),
32
+ MemeCaptain::TextPos.new('jumped over the lazy dog', 100, 400, 200, 100,
33
+ :font => 'Impact-Regular'),
34
+ MemeCaptain::TextPos.new('test', 10, 10, 50, 25)
35
+ ])
14
36
  i.display
15
37
  i.write('out.jpg')
16
38
  end
17
39
  ```
18
40
 
41
+ Text box sizes and positions can be specified as pixels (the origin is the top
42
+ left corner of the image) or as floats which are percentages of the image
43
+ width and height. The x and y coordinates of a text box are the coordinates
44
+ of its top left corner.
45
+
19
46
  Also includes a Sinatra app that exposes the API over HTTP which is currently
20
47
  running http://memecaptain.com/
21
48
 
@@ -24,36 +51,34 @@ You can use the memecaptain.com API if you prefer it to using the gem.
24
51
  Simplest API:
25
52
 
26
53
  ```
27
- http://memecaptain.com/i?u=<url encoded source image url>&tt=<url encoded top text>&tb=<url encoded bottom text>
54
+ http://memecaptain.com/i?u=<url encoded source image url>&t1=<url encoded top text>&t2=<url encoded bottom text>
28
55
  ```
29
56
 
30
57
  Example:
31
58
 
32
59
  ```
33
- http://memecaptain.com/i?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&tt=sure+i%27ll+test&tb=the+api
60
+ http://memecaptain.com/i?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&t1=sure+i%27ll+test&t2=the+api
34
61
  ```
35
62
 
36
- ![Sure I'll test the API](http://memecaptain.com/i?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&tt=sure+i%27ll+test&tb=the+api)
63
+ ![Sure I'll test the API](http://memecaptain.com/i?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&t1=sure+i%27ll+test&t2=the+api)
37
64
 
38
65
  If you want better error messages, use this which will return JSON:
39
66
 
40
67
  ```
41
- http://memecaptain.com/g?u=<url encoded source image url>&tt=<url encoded top text>&tb=<url encoded bottom text>
68
+ http://memecaptain.com/g?u=<url encoded source image url>&t1=<url encoded top text>&t2=<url encoded bottom text>
42
69
  ```
43
70
 
44
71
  Example:
45
72
 
46
73
  ```
47
- http://memecaptain.com/g?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&tt=sure+i%27ll+test&tb=the+api
74
+ http://memecaptain.com/g?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&t1=sure+i%27ll+test&t2=the+api
48
75
  ```
49
76
 
50
- Note: tempUrl is deprecated and will now always be the same as permUrl. It is
51
- left for compability with older clients.
52
-
53
-
54
77
  ```json
55
78
  {
56
- permUrl: "http://memecaptain.com/i?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&tt=sure+i%27ll+test&tb=the+api"
57
- tempUrl: "http://memecaptain.com/tmp/de55f7a78c6559d4a24ef3e72e2de89992b82695.jpeg"
79
+ imageUrl: "http://memecaptain.com/c7757f.jpg"
58
80
  }
59
81
  ```
82
+
83
+ Optional parameters t1x, t1y, t1w, t1h, t2x, t2y, t2w, t2h can be added to
84
+ position and size text (see example above).
data/bin/memecaptain CHANGED
@@ -50,7 +50,9 @@ unless ARGV.empty?
50
50
  ].include? k
51
51
  end
52
52
 
53
- output_image = MemeCaptain.meme(
53
+ meme_options = Hash[meme_options] if meme_options.is_a?(Array)
54
+
55
+ output_image = MemeCaptain.meme_top_bottom(
54
56
  input_io, options[:top_text], options[:bottom_text], meme_options)
55
57
 
56
58
  input_io.close
data/config.ru CHANGED
@@ -3,6 +3,7 @@ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
3
3
  require 'mongo'
4
4
  require 'mongo_mapper'
5
5
  require 'rack'
6
+ require 'rack/rewrite'
6
7
 
7
8
  require 'meme_captain'
8
9
 
@@ -18,8 +19,28 @@ MemeCaptain::MemeData.ensure_index :meme_id
18
19
 
19
20
  MemeCaptain::MemeData.ensure_index [
20
21
  [:source_url, 1],
21
- [:top_text, 1],
22
- [:bottom_text, 1],
22
+ [:texts, 1],
23
23
  ]
24
24
 
25
+ MemeCaptain::SourceFetchFail.ensure_index :url
26
+
27
+ use Rack::Rewrite do
28
+ rewrite %r{/([gi])\?(.+)}, lambda { |match, rack_env|
29
+ result = match[0]
30
+
31
+ if match[2].index('tt=') or match[2].index('tb=')
32
+ q = Rack::Utils.parse_query(match[2])
33
+ if q.key?('tt') or q.key?('tb')
34
+ q['t1'] = q.delete('tt') if q.key?('tt')
35
+ q['t2'] = q.delete('tb') if q.key?('tb')
36
+ new_q = q.map { |k,v|
37
+ "#{Rack::Utils.escape(k)}=#{Rack::Utils.escape(v)}" }.join('&')
38
+ result = "#{match[1]}?#{new_q}"
39
+ end
40
+ end
41
+
42
+ result
43
+ }
44
+ end
45
+
25
46
  run MemeCaptain::Server
data/lib/meme_captain.rb CHANGED
@@ -6,4 +6,6 @@ require 'meme_captain/image_list'
6
6
  require 'meme_captain/meme'
7
7
  require 'meme_captain/meme_data'
8
8
  require 'meme_captain/server'
9
+ require 'meme_captain/source_fetch_fail'
10
+ require 'meme_captain/text_pos'
9
11
  require 'meme_captain/version'
@@ -1,5 +1,6 @@
1
1
  require 'meme_captain/image_list/cache'
2
2
  require 'meme_captain/image_list/fetch'
3
+ require 'meme_captain/image_list/fetch_error'
3
4
  require 'meme_captain/image_list/watermark'
4
5
 
5
6
  require 'meme_captain/image_list/source_image'
@@ -13,7 +13,7 @@ module MemeCaptain
13
13
  c.useragent = 'Meme Captain http://memecaptain.com/'
14
14
  end
15
15
  unless curl.response_code == 200
16
- raise "Error loading source image url #{url}"
16
+ raise FetchError.new(curl.response_code)
17
17
  end
18
18
 
19
19
  from_blob curl.body_str
@@ -0,0 +1,17 @@
1
+ module MemeCaptain
2
+
3
+ module ImageList
4
+
5
+ # Error for source image fetch failures.
6
+ class FetchError < StandardError
7
+
8
+ def initialize(response_code)
9
+ @response_code = response_code
10
+ end
11
+
12
+ attr_accessor :response_code
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -6,19 +6,12 @@ module MemeCaptain
6
6
 
7
7
  # Create a meme image.
8
8
  #
9
- # Input can be an IO object or a blob of data.
9
+ # Input can be an IO object or a blob of data. text_poss is an enumerable
10
+ # of TextPos objects containing text, position and style options.
10
11
  #
11
12
  # Options:
12
- # max_lines - maximum number of text lines per caption
13
- # min_pointsize - minimum point size
14
13
  # super_sample - work this many times larger before shrinking
15
- # text_height_pct - float percentage of image height for each caption
16
- # text_width_pct - float percentage of image width for each caption
17
- #
18
- # Any other options will be set on the Draw objects for the text and
19
- # can be used to control text fill, stroke, etc. See RMagick annotate
20
- # attributes.
21
- def meme(input, top_text, bottom_text, options={})
14
+ def meme(input, text_poss, options={})
22
15
  img = Magick::ImageList.new
23
16
  if input.respond_to?(:read)
24
17
  img.from_blob(input.read)
@@ -26,27 +19,7 @@ module MemeCaptain
26
19
  img.from_blob(input)
27
20
  end
28
21
 
29
- options = {
30
- :max_lines => 16,
31
- :min_pointsize => 12,
32
- :super_sample => 2.0,
33
- :text_height_pct => 0.25,
34
- :text_width_pct => 0.9,
35
-
36
- # Draw options
37
- :fill => 'white',
38
- :font => 'Impact',
39
- :stroke => 'black',
40
- :stroke_width => 8,
41
- }.merge(options)
42
-
43
- max_lines = options.delete(:max_lines)
44
- super_sample = options.delete(:super_sample)
45
- min_pointsize = options.delete(:min_pointsize) * super_sample
46
-
47
- text_width = img.page.width * options.delete(:text_width_pct) * super_sample
48
- text_height = img.page.height * options.delete(:text_height_pct) *
49
- super_sample
22
+ super_sample = options[:super_sample] || 2.0
50
23
 
51
24
  text_layer = Magick::Image.new(
52
25
  img.page.width * super_sample, img.page.height * super_sample) {
@@ -54,38 +27,54 @@ module MemeCaptain
54
27
  self.density = 72.0 * super_sample
55
28
  }
56
29
 
57
- draw = Magick::Draw.new {
58
- options.each { |k,v| self.send("#{k}=", v) }
59
- }
30
+ text_poss.each do |text_pos|
31
+ caption = Caption.new(text_pos.text)
60
32
 
61
- draw.extend(Draw)
33
+ if caption.drawable?
34
+ wrap_tries = (1..text_pos.max_lines).map { |num_lines|
35
+ caption.wrap(num_lines).upcase.annotate_quote
36
+ }.uniq
62
37
 
63
- [
64
- [Caption.new(top_text), Magick::NorthGravity],
65
- [Caption.new(bottom_text), Magick::SouthGravity],
66
- ].select { |x| x[0].drawable? }.each do |caption, gravity|
67
- wrap_tries = (1..max_lines).map { |num_lines|
68
- caption.wrap(num_lines).upcase.annotate_quote
69
- }.uniq
38
+ text_x = (text_pos.x.is_a?(Float) ?
39
+ img.page.width * text_pos.x : text_pos.x) * super_sample
70
40
 
71
- choices = wrap_tries.map do |wrap_try|
72
- pointsize, metrics = draw.calc_pointsize(
73
- text_width, text_height, wrap_try, min_pointsize)
41
+ text_y = (text_pos.y.is_a?(Float) ?
42
+ img.page.height * text_pos.y : text_pos.y) * super_sample
74
43
 
75
- CaptionChoice.new(pointsize, metrics, wrap_try, text_width,
76
- text_height)
77
- end
44
+ text_width = (text_pos.width.is_a?(Float) ?
45
+ img.page.width * text_pos.width : text_pos.width) * super_sample
46
+
47
+ text_height = (text_pos.height.is_a?(Float) ?
48
+ img.page.height * text_pos.height : text_pos.height) * super_sample
49
+
50
+ min_pointsize = text_pos.min_pointsize * super_sample
78
51
 
79
- choice = choices.max
52
+ draw = Magick::Draw.new {
53
+ text_pos.draw_options.each { |k,v| self.send("#{k}=", v) }
54
+ }
80
55
 
81
- draw.gravity = gravity
82
- draw.pointsize = choice.pointsize
56
+ draw.extend Draw
83
57
 
84
- draw.stroke = options[:stroke]
85
- draw.annotate text_layer, 0, 0, 0, 0, choice.text
58
+ choices = wrap_tries.map do |wrap_try|
59
+ pointsize, metrics = draw.calc_pointsize(text_width, text_height,
60
+ wrap_try, min_pointsize)
86
61
 
87
- draw.stroke = 'none'
88
- draw.annotate text_layer, 0, 0, 0, 0, choice.text
62
+ CaptionChoice.new(pointsize, metrics, wrap_try, text_width,
63
+ text_height)
64
+ end
65
+
66
+ choice = choices.max
67
+
68
+ draw.pointsize = choice.pointsize
69
+
70
+ draw.annotate text_layer, text_width, text_height, text_x, text_y,
71
+ choice.text
72
+
73
+ draw.stroke = 'none'
74
+
75
+ draw.annotate text_layer, text_width, text_height, text_x, text_y,
76
+ choice.text
77
+ end
89
78
  end
90
79
 
91
80
  text_layer.resize!(1.0 / super_sample)
@@ -99,4 +88,12 @@ module MemeCaptain
99
88
 
100
89
  end
101
90
 
91
+ # Shortcut to generate a typical meme with text at the top and bottom.
92
+ def meme_top_bottom(input, top_text, bottom_text, options={})
93
+ meme(input, [
94
+ TextPos.new(top_text, 0.05, 0, 0.9, 0.25, options),
95
+ TextPos.new(bottom_text, 0.05, 0.75, 0.9, 0.25, options)
96
+ ])
97
+ end
98
+
102
99
  end
@@ -14,8 +14,7 @@ module MemeCaptain
14
14
 
15
15
  key :source_url, String
16
16
  key :source_fs_path, String
17
- key :top_text, String
18
- key :bottom_text, String
17
+ key :texts, Array
19
18
 
20
19
  key :request_count, Integer
21
20
  key :last_request, Time
@@ -15,22 +15,51 @@ module MemeCaptain
15
15
 
16
16
  get '/' do
17
17
  @u = params[:u]
18
- @tt= params[:tt]
19
- @tb = params[:tb]
18
+
19
+ @t1 = params[:t1]
20
+ @t1x = params[:t1x]
21
+ @t1y = params[:t1y]
22
+ @t1w = params[:t1w]
23
+ @t1h = params[:t1h]
24
+
25
+ @t2 = params[:t2]
26
+ @t2x = params[:t2x]
27
+ @t2y = params[:t2y]
28
+ @t2w = params[:t2w]
29
+ @t2h = params[:t2h]
20
30
 
21
31
  @root_url = url('/')
22
32
 
23
33
  erb :index
24
34
  end
25
35
 
36
+ def convert_metric(metric, default)
37
+ case
38
+ when metric.to_s.empty?; default
39
+ when metric.index('.'); metric.to_f
40
+ else; metric.to_i
41
+ end
42
+ end
43
+
26
44
  def normalize_params(p)
27
45
  result = {
28
46
  'u' => p[:u],
47
+
29
48
  # convert to empty string if null
30
- 'tt' => p[:tt].to_s,
31
- 'tb' => p[:tb].to_s,
49
+ 't1' => p[:t1].to_s,
50
+ 't2' => p[:t2].to_s,
32
51
  }
33
52
 
53
+ result['t1x'] = convert_metric(p[:t1x], 0.05)
54
+ result['t1y'] = convert_metric(p[:t1y], 0)
55
+ result['t1w'] = convert_metric(p[:t1w], 0.9)
56
+ result['t1h'] = convert_metric(p[:t1h], 0.25)
57
+
58
+ result['t2x'] = convert_metric(p[:t2x], 0.05)
59
+ result['t2y'] = convert_metric(p[:t2y], 0.75)
60
+ result['t2w'] = convert_metric(p[:t2w], 0.9)
61
+ result['t2h'] = convert_metric(p[:t2h], 0.25)
62
+
34
63
  # if the id of an existing meme is passed in as the source url, use the
35
64
  # source image of that meme for the source image
36
65
  if result['u'][%r{^[a-f0-9]+\.(?:gif|jpg|png)$}]
@@ -48,23 +77,54 @@ module MemeCaptain
48
77
 
49
78
  if existing = MemeData.first(
50
79
  :source_url => norm_params[:u],
51
- :top_text => norm_params[:tt],
52
- :bottom_text => norm_params[:tb]
80
+
81
+ 'texts.0.text' => norm_params[:t1],
82
+ 'texts.0.x' => norm_params[:t1x],
83
+ 'texts.0.y' => norm_params[:t1y],
84
+ 'texts.0.w' => norm_params[:t1w],
85
+ 'texts.0.h' => norm_params[:t1h],
86
+
87
+ 'texts.1.text' => norm_params[:t2],
88
+ 'texts.1.x' => norm_params[:t2x],
89
+ 'texts.1.y' => norm_params[:t2y],
90
+ 'texts.1.w' => norm_params[:t2w],
91
+ 'texts.1.h' => norm_params[:t2h]
53
92
  )
54
93
  existing
55
94
  else
56
95
  if same_source = MemeData.find_by_source_url(norm_params[:u])
57
96
  source_fs_path = same_source.source_fs_path
58
97
  else
59
- source_img = ImageList::SourceImage.new
60
- source_img.fetch! norm_params[:u]
61
- source_img.prepare! settings.source_img_max_side, settings.watermark
62
- source_fs_path = source_img.cache(norm_params[:u], 'source_cache')
98
+ if source_fetch_fail = SourceFetchFail.find_by_url(norm_params[:u])
99
+ source_fetch_fail.requested!
100
+ halt 500, 'Error loading source image url'
101
+ else
102
+ source_img = ImageList::SourceImage.new
103
+ begin
104
+ source_img.fetch! norm_params[:u]
105
+ rescue => error
106
+ SourceFetchFail.new(
107
+ :attempt_count => 1,
108
+ :orig_ip => request.ip,
109
+ :response_code => error.respond_to?(:response_code) ?
110
+ error.response_code : nil,
111
+ :url => norm_params[:u]
112
+ ).save!
113
+ halt 500, 'Error loading source image url'
114
+ end
115
+ source_img.prepare! settings.source_img_max_side, settings.watermark
116
+ source_fs_path = source_img.cache(norm_params[:u], 'source_cache')
117
+ end
63
118
  end
64
119
 
65
120
  open(source_fs_path, 'rb') do |source_io|
66
- meme_img = MemeCaptain.meme(source_io, norm_params[:tt],
67
- norm_params[:tb])
121
+ t1 = TextPos.new(norm_params[:t1], norm_params[:t1x],
122
+ norm_params[:t1y], norm_params[:t1w], norm_params[:t1h])
123
+
124
+ t2 = TextPos.new(norm_params[:t2], norm_params[:t2x],
125
+ norm_params[:t2y], norm_params[:t2w], norm_params[:t2h])
126
+
127
+ meme_img = MemeCaptain.meme(source_io, [t1, t2])
68
128
  meme_img.extend ImageList::Cache
69
129
 
70
130
  # convert non-animated gifs to png
@@ -95,8 +155,20 @@ module MemeCaptain
95
155
 
96
156
  :source_url => norm_params[:u],
97
157
  :source_fs_path => source_fs_path,
98
- :top_text => norm_params[:tt],
99
- :bottom_text => norm_params[:tb],
158
+
159
+ :texts => [{
160
+ :text => norm_params[:t1],
161
+ :x => norm_params[:t1x],
162
+ :y => norm_params[:t1y],
163
+ :w => norm_params[:t1w],
164
+ :h => norm_params[:t1h],
165
+ }, {
166
+ :text => norm_params[:t2],
167
+ :x => norm_params[:t2x],
168
+ :y => norm_params[:t2y],
169
+ :w => norm_params[:t2w],
170
+ :h => norm_params[:t2h],
171
+ }],
100
172
 
101
173
  :request_count => 0,
102
174
 
@@ -117,22 +189,11 @@ module MemeCaptain
117
189
  begin
118
190
  meme_data = gen(params)
119
191
 
120
- meme_url = url("/#{meme_data.meme_id}")
121
-
122
- template_query = [
123
- [:u, meme_data.meme_id],
124
- [:tt, meme_data.top_text],
125
- [:tb, meme_data.bottom_text],
126
- ].map { |k,v|
127
- "#{Rack::Utils.escape(k)}=#{Rack::Utils.escape(v)}" }.join('&')
128
-
129
192
  [200, { 'Content-Type' => 'application/json' }, {
130
- 'tempUrl' => meme_url,
131
- 'permUrl' => meme_url,
132
- 'templateUrl' => url("/?#{template_query}"),
193
+ 'imageUrl' => url("/#{meme_data.meme_id}")
133
194
  }.to_json]
134
195
  rescue => error
135
- [500, { 'Content-Type' => 'text/plain' }, error.to_s]
196
+ [500, { 'Content-Type' => 'text/plain' }, 'Error generating image']
136
197
  end
137
198
  end
138
199