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 +14 -0
- data/README.md +38 -13
- data/bin/memecaptain +3 -1
- data/config.ru +23 -2
- data/lib/meme_captain.rb +2 -0
- data/lib/meme_captain/image_list.rb +1 -0
- data/lib/meme_captain/image_list/fetch.rb +1 -1
- data/lib/meme_captain/image_list/fetch_error.rb +17 -0
- data/lib/meme_captain/meme.rb +52 -55
- data/lib/meme_captain/meme_data.rb +1 -2
- data/lib/meme_captain/server.rb +88 -27
- data/lib/meme_captain/source_fetch_fail.rb +28 -0
- data/lib/meme_captain/text_pos.rb +45 -0
- data/lib/meme_captain/version.rb +1 -1
- data/meme_captain.gemspec +5 -0
- data/public/css/screen.css +66 -0
- data/public/js/fabric.min.js +7 -0
- data/spec/meme_captain_spec.rb +13 -0
- data/views/index.erb +307 -112
- metadata +40 -7
- data/public/1.gif +0 -0
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
|
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.
|
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>&
|
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&
|
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&
|
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>&
|
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&
|
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
|
-
|
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
|
-
|
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
|
-
[:
|
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
@@ -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
|
data/lib/meme_captain/meme.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
}
|
30
|
+
text_poss.each do |text_pos|
|
31
|
+
caption = Caption.new(text_pos.text)
|
60
32
|
|
61
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
52
|
+
draw = Magick::Draw.new {
|
53
|
+
text_pos.draw_options.each { |k,v| self.send("#{k}=", v) }
|
54
|
+
}
|
80
55
|
|
81
|
-
|
82
|
-
draw.pointsize = choice.pointsize
|
56
|
+
draw.extend Draw
|
83
57
|
|
84
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
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
|
data/lib/meme_captain/server.rb
CHANGED
@@ -15,22 +15,51 @@ module MemeCaptain
|
|
15
15
|
|
16
16
|
get '/' do
|
17
17
|
@u = params[:u]
|
18
|
-
|
19
|
-
@
|
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
|
-
'
|
31
|
-
'
|
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
|
-
|
52
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
67
|
-
norm_params[:
|
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
|
-
|
99
|
-
:
|
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
|
-
'
|
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' },
|
196
|
+
[500, { 'Content-Type' => 'text/plain' }, 'Error generating image']
|
136
197
|
end
|
137
198
|
end
|
138
199
|
|