meme_captain 0.1.0 → 0.1.1
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 +62 -2
- data/Gemfile +3 -0
- data/README.md +6 -1
- data/config.ru +4 -0
- data/doc/lightweight_front_end.md +64 -0
- data/doc/setup.md +13 -0
- data/lib/meme_captain/image_list/fetch.rb +2 -0
- data/lib/meme_captain/meme.rb +16 -4
- data/lib/meme_captain/memebg.rb +62 -0
- data/lib/meme_captain/norm_params.rb +92 -0
- data/lib/meme_captain/pretty_format.rb +12 -0
- data/lib/meme_captain/server.rb +134 -74
- data/lib/meme_captain/upload.rb +30 -0
- data/lib/meme_captain/version.rb +1 -1
- data/lib/meme_captain.rb +4 -1
- data/meme_captain.gemspec +2 -0
- data/public/css/screen.css +14 -0
- data/public/favicon.ico +0 -0
- data/public/js/fabric.min.js +3 -3
- data/public/js/jquery-1.7.2.min.js +4 -0
- data/public/js/meme_captain.js +403 -0
- data/public/source_images.json +65 -1
- data/public/thumbs.jpg +0 -0
- data/public/thumbs_1330486916.jpg +0 -0
- data/public/thumbs_1333591668.jpg +0 -0
- data/public/thumbs_1334189407.jpg +0 -0
- data/public/thumbs_1334973608.jpg +0 -0
- data/script/thumb_sprites.rb +57 -14
- data/spec/caption_choice_spec.rb +53 -0
- data/spec/caption_spec.rb +45 -0
- data/spec/image_list/fetch_spec.rb +33 -0
- data/spec/meme_captain_spec.rb +8 -2
- data/spec/memebg_spec.rb +14 -0
- data/spec/norm_params_spec.rb +223 -0
- data/spec/pretty_format_spec.rb +9 -0
- data/spec/text_pos_spec.rb +29 -0
- data/views/index.erb +14 -335
- metadata +150 -161
- data/lib/meme_captain/file_body.rb +0 -15
data/lib/meme_captain/server.rb
CHANGED
|
@@ -10,9 +10,16 @@ module MemeCaptain
|
|
|
10
10
|
|
|
11
11
|
set :root, File.expand_path(File.join('..', '..'), File.dirname(__FILE__))
|
|
12
12
|
set :source_img_max_side, 800
|
|
13
|
+
set :upload_prefix, 'up/'
|
|
13
14
|
set :watermark, Magick::ImageList.new(File.expand_path(
|
|
14
15
|
File.join('..', '..', 'watermark.png'), File.dirname(__FILE__)))
|
|
15
16
|
|
|
17
|
+
# uncomment this to allow other sites to use this site's backend for
|
|
18
|
+
# generating images, used to allow third-party static only sites
|
|
19
|
+
# to generate images using memecaptain.com
|
|
20
|
+
|
|
21
|
+
# set :protection, :except => :json_csrf
|
|
22
|
+
|
|
16
23
|
get '/' do
|
|
17
24
|
@u = params[:u]
|
|
18
25
|
|
|
@@ -33,107 +40,110 @@ module MemeCaptain
|
|
|
33
40
|
erb :index
|
|
34
41
|
end
|
|
35
42
|
|
|
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
|
-
|
|
44
43
|
def normalize_params(p)
|
|
45
|
-
result =
|
|
46
|
-
'u' => p[:u],
|
|
47
|
-
|
|
48
|
-
# convert to empty string if null
|
|
49
|
-
't1' => p[:t1].to_s,
|
|
50
|
-
't2' => p[:t2].to_s,
|
|
51
|
-
}
|
|
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)
|
|
44
|
+
result = NormParams.new(p)
|
|
62
45
|
|
|
63
46
|
# if the id of an existing meme is passed in as the source url, use the
|
|
64
47
|
# source image of that meme for the source image
|
|
65
|
-
if result
|
|
66
|
-
if existing_as_source = MemeData.find_by_meme_id(result
|
|
67
|
-
result
|
|
48
|
+
if result.u[%r{^[a-f0-9]+\.(?:gif|jpg|png)$}]
|
|
49
|
+
if existing_as_source = MemeData.find_by_meme_id(result.u)
|
|
50
|
+
result.u = existing_as_source.source_url
|
|
68
51
|
end
|
|
69
52
|
end
|
|
70
53
|
|
|
71
|
-
|
|
72
|
-
Hash.new { |hash,key| hash[key.to_s] if Symbol === key }.merge(result)
|
|
54
|
+
result
|
|
73
55
|
end
|
|
74
56
|
|
|
75
57
|
def gen(p)
|
|
58
|
+
logger.debug "params:\n#{MemeCaptain.pretty_format(p)}"
|
|
76
59
|
norm_params = normalize_params(p)
|
|
60
|
+
logger.debug 'normalized params:'
|
|
61
|
+
logger.debug(MemeCaptain.pretty_format(norm_params))
|
|
77
62
|
|
|
78
63
|
if existing = MemeData.first(
|
|
79
|
-
:source_url => norm_params
|
|
80
|
-
|
|
81
|
-
'texts.0.text' => norm_params
|
|
82
|
-
'texts.0.x' => norm_params
|
|
83
|
-
'texts.0.y' => norm_params
|
|
84
|
-
'texts.0.w' => norm_params
|
|
85
|
-
'texts.0.h' => norm_params
|
|
86
|
-
|
|
87
|
-
'texts.1.text' => norm_params
|
|
88
|
-
'texts.1.x' => norm_params
|
|
89
|
-
'texts.1.y' => norm_params
|
|
90
|
-
'texts.1.w' => norm_params
|
|
91
|
-
'texts.1.h' => norm_params
|
|
64
|
+
:source_url => norm_params.u,
|
|
65
|
+
|
|
66
|
+
'texts.0.text' => norm_params.t1,
|
|
67
|
+
'texts.0.x' => norm_params.t1x,
|
|
68
|
+
'texts.0.y' => norm_params.t1y,
|
|
69
|
+
'texts.0.w' => norm_params.t1w,
|
|
70
|
+
'texts.0.h' => norm_params.t1h,
|
|
71
|
+
|
|
72
|
+
'texts.1.text' => norm_params.t2,
|
|
73
|
+
'texts.1.x' => norm_params.t2x,
|
|
74
|
+
'texts.1.y' => norm_params.t2y,
|
|
75
|
+
'texts.1.w' => norm_params.t2w,
|
|
76
|
+
'texts.1.h' => norm_params.t2h
|
|
92
77
|
)
|
|
78
|
+
logger.debug 'found existing meme:'
|
|
79
|
+
logger.debug(MemeCaptain.pretty_format(existing))
|
|
93
80
|
existing
|
|
94
81
|
else
|
|
95
|
-
if same_source = MemeData.find_by_source_url(norm_params
|
|
82
|
+
if same_source = MemeData.find_by_source_url(norm_params.u)
|
|
83
|
+
logger.debug 'found existing source image'
|
|
96
84
|
source_fs_path = same_source.source_fs_path
|
|
85
|
+
elsif (norm_params.u.index(settings.upload_prefix) == 0) and
|
|
86
|
+
(upload = Upload.find_by_upload_id(
|
|
87
|
+
norm_params.u[settings.upload_prefix.size..-1]))
|
|
88
|
+
|
|
89
|
+
logger.debug 'source image is upload:'
|
|
90
|
+
logger.debug MemeCaptain.pretty_format(upload)
|
|
91
|
+
source_fs_path = upload.fs_path
|
|
97
92
|
else
|
|
98
|
-
if source_fetch_fail = SourceFetchFail.find_by_url(norm_params
|
|
93
|
+
if source_fetch_fail = SourceFetchFail.find_by_url(norm_params.u)
|
|
94
|
+
logger.debug 'skipping fetch of previously failed source image:'
|
|
95
|
+
logger.debug(MemeCaptain.pretty_format(source_fetch_fail))
|
|
99
96
|
source_fetch_fail.requested!
|
|
100
97
|
halt 500, 'Error loading source image url'
|
|
101
98
|
else
|
|
102
99
|
source_img = ImageList::SourceImage.new
|
|
103
100
|
begin
|
|
104
|
-
|
|
101
|
+
logger.debug "fetch source image: #{norm_params.u}"
|
|
102
|
+
source_img.fetch! norm_params.u
|
|
105
103
|
rescue => error
|
|
106
|
-
SourceFetchFail.new(
|
|
104
|
+
new_source_fetch_fail = SourceFetchFail.new(
|
|
107
105
|
:attempt_count => 1,
|
|
108
106
|
:orig_ip => request.ip,
|
|
109
107
|
:response_code => error.respond_to?(:response_code) ?
|
|
110
108
|
error.response_code : nil,
|
|
111
|
-
:url => norm_params
|
|
112
|
-
)
|
|
109
|
+
:url => norm_params.u
|
|
110
|
+
)
|
|
111
|
+
logger.debug 'source image fetch failed:'
|
|
112
|
+
logger.debug(MemeCaptain.pretty_format(new_source_fetch_fail))
|
|
113
|
+
new_source_fetch_fail.save!
|
|
113
114
|
halt 500, 'Error loading source image url'
|
|
114
115
|
end
|
|
115
116
|
source_img.prepare! settings.source_img_max_side, settings.watermark
|
|
116
|
-
source_fs_path = source_img.cache(norm_params
|
|
117
|
+
source_fs_path = source_img.cache(norm_params.u, 'source_cache')
|
|
118
|
+
source_img.each { |frame| frame.destroy! }
|
|
117
119
|
end
|
|
118
120
|
end
|
|
119
121
|
|
|
122
|
+
logger.debug "source image filesystem path: #{source_fs_path}"
|
|
123
|
+
|
|
120
124
|
open(source_fs_path, 'rb') do |source_io|
|
|
121
|
-
t1 = TextPos.new(norm_params
|
|
122
|
-
norm_params
|
|
125
|
+
t1 = TextPos.new(norm_params.t1, norm_params.t1x, norm_params.t1y,
|
|
126
|
+
norm_params.t1w, norm_params.t1h)
|
|
123
127
|
|
|
124
|
-
t2 = TextPos.new(norm_params
|
|
125
|
-
norm_params
|
|
128
|
+
t2 = TextPos.new(norm_params.t2, norm_params.t2x, norm_params.t2y,
|
|
129
|
+
norm_params.t2w, norm_params.t2h)
|
|
126
130
|
|
|
127
131
|
meme_img = MemeCaptain.meme(source_io, [t1, t2])
|
|
128
132
|
meme_img.extend ImageList::Cache
|
|
129
133
|
|
|
134
|
+
# convert source images in formats other than jpeg, gif or png
|
|
135
|
+
# to png
|
|
136
|
+
unless %w{JPEG GIF PNG}.include?(meme_img.format)
|
|
137
|
+
meme_img.format = 'PNG'
|
|
138
|
+
end
|
|
139
|
+
|
|
130
140
|
# convert non-animated gifs to png
|
|
131
141
|
if meme_img.format == 'GIF' and meme_img.size == 1
|
|
132
142
|
meme_img.format = 'PNG'
|
|
133
143
|
end
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
meme_hash = Digest::SHA1.hexdigest(
|
|
145
|
+
sig = norm_params.signature
|
|
146
|
+
meme_hash = Digest::SHA1.hexdigest(sig)
|
|
137
147
|
|
|
138
148
|
meme_id = nil
|
|
139
149
|
(6..meme_hash.size).each do |len|
|
|
@@ -141,7 +151,9 @@ module MemeCaptain
|
|
|
141
151
|
break unless MemeData.where(:meme_id => meme_id).count > 0
|
|
142
152
|
end
|
|
143
153
|
|
|
144
|
-
meme_fs_path = meme_img.cache(
|
|
154
|
+
meme_fs_path = meme_img.cache(sig, File.join('public', 'meme'))
|
|
155
|
+
|
|
156
|
+
logger.debug "meme filesystem path: #{meme_fs_path}"
|
|
145
157
|
|
|
146
158
|
meme_img.write(meme_fs_path) {
|
|
147
159
|
self.quality = 100
|
|
@@ -153,21 +165,21 @@ module MemeCaptain
|
|
|
153
165
|
:mime_type => meme_img.mime_type,
|
|
154
166
|
:size => File.size(meme_fs_path),
|
|
155
167
|
|
|
156
|
-
:source_url => norm_params
|
|
168
|
+
:source_url => norm_params.u,
|
|
157
169
|
:source_fs_path => source_fs_path,
|
|
158
170
|
|
|
159
171
|
:texts => [{
|
|
160
|
-
:text => norm_params
|
|
161
|
-
:x => norm_params
|
|
162
|
-
:y => norm_params
|
|
163
|
-
:w => norm_params
|
|
164
|
-
:h => norm_params
|
|
172
|
+
:text => norm_params.t1,
|
|
173
|
+
:x => norm_params.t1x,
|
|
174
|
+
:y => norm_params.t1y,
|
|
175
|
+
:w => norm_params.t1w,
|
|
176
|
+
:h => norm_params.t1h,
|
|
165
177
|
}, {
|
|
166
|
-
:text => norm_params
|
|
167
|
-
:x => norm_params
|
|
168
|
-
:y => norm_params
|
|
169
|
-
:w => norm_params
|
|
170
|
-
:h => norm_params
|
|
178
|
+
:text => norm_params.t2,
|
|
179
|
+
:x => norm_params.t2x,
|
|
180
|
+
:y => norm_params.t2y,
|
|
181
|
+
:w => norm_params.t2w,
|
|
182
|
+
:h => norm_params.t2h,
|
|
171
183
|
}],
|
|
172
184
|
|
|
173
185
|
:request_count => 0,
|
|
@@ -175,6 +187,10 @@ module MemeCaptain
|
|
|
175
187
|
:creator_ip => request.ip
|
|
176
188
|
)
|
|
177
189
|
|
|
190
|
+
meme_img.each { |frame| frame.destroy! }
|
|
191
|
+
|
|
192
|
+
logger.debug "meme data:\n#{MemeCaptain.pretty_format(meme_data)}"
|
|
193
|
+
|
|
178
194
|
meme_data.save! :safe => true
|
|
179
195
|
|
|
180
196
|
meme_data
|
|
@@ -184,15 +200,16 @@ module MemeCaptain
|
|
|
184
200
|
end
|
|
185
201
|
|
|
186
202
|
get '/g' do
|
|
187
|
-
raise Sinatra::NotFound if params[:u].to_s.empty?
|
|
188
|
-
|
|
189
203
|
begin
|
|
190
204
|
meme_data = gen(params)
|
|
191
205
|
|
|
192
206
|
[200, { 'Content-Type' => 'application/json' }, {
|
|
193
|
-
'imageUrl' => url("/#{meme_data.meme_id}")
|
|
207
|
+
'imageUrl' => url("/#{meme_data.meme_id}"),
|
|
208
|
+
'templateUrl' => url("/?u=#{Rack::Utils.escape(meme_data.meme_id)}"),
|
|
194
209
|
}.to_json]
|
|
195
210
|
rescue => error
|
|
211
|
+
logger.error "error generating image: #{error.class} #{error.message}"
|
|
212
|
+
logger.error(MemeCaptain.pretty_format(error.backtrace))
|
|
196
213
|
[500, { 'Content-Type' => 'text/plain' }, 'Error generating image']
|
|
197
214
|
end
|
|
198
215
|
end
|
|
@@ -200,9 +217,7 @@ module MemeCaptain
|
|
|
200
217
|
def serve_img(meme_data)
|
|
201
218
|
meme_data.requested!
|
|
202
219
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
FileBody.new meme_data.fs_path
|
|
220
|
+
send_file meme_data.fs_path, :type => meme_data.mime_type
|
|
206
221
|
end
|
|
207
222
|
|
|
208
223
|
get '/i' do
|
|
@@ -219,6 +234,51 @@ module MemeCaptain
|
|
|
219
234
|
end
|
|
220
235
|
end
|
|
221
236
|
|
|
237
|
+
post '/upload' do
|
|
238
|
+
redirect('/') unless params[:upload]
|
|
239
|
+
|
|
240
|
+
img = ImageList::SourceImage.new
|
|
241
|
+
img.from_blob(params[:upload][:tempfile].read)
|
|
242
|
+
img.prepare! settings.source_img_max_side, settings.watermark
|
|
243
|
+
fs_path = img.cache(params[:upload][:filename], 'source_cache')
|
|
244
|
+
|
|
245
|
+
filename_hash = Digest::SHA1.hexdigest(params[:upload][:filename])
|
|
246
|
+
|
|
247
|
+
len = 6
|
|
248
|
+
upload_id = filename_hash[0,len]
|
|
249
|
+
while Upload.where(:upload_id => upload_id).count > 0
|
|
250
|
+
upload_id = if len < filename_hash.size
|
|
251
|
+
len += 1
|
|
252
|
+
filename_hash[0,len]
|
|
253
|
+
else
|
|
254
|
+
"#{upload_id}0"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
upload = Upload.new(
|
|
259
|
+
:upload_id => upload_id,
|
|
260
|
+
:fs_path => fs_path,
|
|
261
|
+
:mime_type => img.mime_type,
|
|
262
|
+
:size => File.size(fs_path),
|
|
263
|
+
:request_count => 0,
|
|
264
|
+
:creator_ip => request.ip
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
img.each { |frame| frame.destroy! }
|
|
268
|
+
|
|
269
|
+
upload.save! :safe => true
|
|
270
|
+
|
|
271
|
+
redirect "/?u=#{settings.upload_prefix}#{Rack::Utils.escape(upload_id)}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
get %r{/#{settings.upload_prefix}(.+)} do
|
|
275
|
+
if upload = Upload.find_by_upload_id(params[:captures][0])
|
|
276
|
+
serve_img upload
|
|
277
|
+
else
|
|
278
|
+
raise Sinatra::NotFound
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
222
282
|
not_found do
|
|
223
283
|
@root_url = url('/')
|
|
224
284
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'mongo_mapper'
|
|
2
|
+
|
|
3
|
+
module MemeCaptain
|
|
4
|
+
|
|
5
|
+
# Data about uploaded files.
|
|
6
|
+
class Upload
|
|
7
|
+
include MongoMapper::Document
|
|
8
|
+
|
|
9
|
+
set_collection_name 'upload'
|
|
10
|
+
|
|
11
|
+
key :upload_id, String
|
|
12
|
+
key :fs_path, String
|
|
13
|
+
key :mime_type, String
|
|
14
|
+
key :size, Integer
|
|
15
|
+
|
|
16
|
+
key :request_count, Integer
|
|
17
|
+
key :last_request, Time
|
|
18
|
+
|
|
19
|
+
key :creator_ip, String
|
|
20
|
+
|
|
21
|
+
timestamps!
|
|
22
|
+
|
|
23
|
+
def requested!
|
|
24
|
+
increment :request_count => 1
|
|
25
|
+
set :last_request => Time.now
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
data/lib/meme_captain/version.rb
CHANGED
data/lib/meme_captain.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
require 'meme_captain/caption'
|
|
2
2
|
require 'meme_captain/caption_choice'
|
|
3
3
|
require 'meme_captain/draw'
|
|
4
|
-
require 'meme_captain/file_body'
|
|
5
4
|
require 'meme_captain/image_list'
|
|
6
5
|
require 'meme_captain/meme'
|
|
6
|
+
require 'meme_captain/memebg'
|
|
7
7
|
require 'meme_captain/meme_data'
|
|
8
|
+
require 'meme_captain/norm_params'
|
|
9
|
+
require 'meme_captain/pretty_format'
|
|
8
10
|
require 'meme_captain/server'
|
|
9
11
|
require 'meme_captain/source_fetch_fail'
|
|
10
12
|
require 'meme_captain/text_pos'
|
|
13
|
+
require 'meme_captain/upload'
|
|
11
14
|
require 'meme_captain/version'
|
data/meme_captain.gemspec
CHANGED
|
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
|
|
|
22
22
|
mongo
|
|
23
23
|
mongo_mapper
|
|
24
24
|
rack
|
|
25
|
+
rack-contrib
|
|
25
26
|
rack-rewrite
|
|
26
27
|
rmagick
|
|
27
28
|
sinatra
|
|
@@ -29,6 +30,7 @@ Gem::Specification.new do |s|
|
|
|
29
30
|
|
|
30
31
|
%w{
|
|
31
32
|
rspec
|
|
33
|
+
webmock
|
|
32
34
|
}.each { |g| s.add_development_dependency g }
|
|
33
35
|
|
|
34
36
|
s.files = `git ls-files`.split("\n")
|
data/public/css/screen.css
CHANGED
|
@@ -4,6 +4,10 @@ body {
|
|
|
4
4
|
font-family : verdana, helvetica, arial, sans-serif;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
a img {
|
|
8
|
+
border : 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
a:link {
|
|
8
12
|
color : #edfc72;
|
|
9
13
|
}
|
|
@@ -64,3 +68,13 @@ input[type=text] {
|
|
|
64
68
|
input[type=button] {
|
|
65
69
|
padding : 4px 8px 4px 8px;
|
|
66
70
|
}
|
|
71
|
+
|
|
72
|
+
input[type=text].attn {
|
|
73
|
+
background-color : #a7e464;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
div.share {
|
|
77
|
+
display : inline-block;
|
|
78
|
+
margin-right : 1em;
|
|
79
|
+
vertical-align : middle;
|
|
80
|
+
}
|
data/public/favicon.ico
ADDED
|
Binary file
|