meme_captain 0.0.9 → 0.1.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/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