meme_captain 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.textile +7 -8
- data/config.ru +5 -0
- data/img_cache/processed/.gitignore +0 -0
- data/img_cache/source/.gitignore +0 -0
- data/lib/meme_captain.rb +4 -40
- data/lib/meme_captain/content_type.rb +16 -0
- data/lib/meme_captain/filesystem_cache.rb +39 -0
- data/lib/meme_captain/meme.rb +52 -0
- data/lib/meme_captain/server.rb +57 -0
- data/meme_captain.gemspec +3 -1
- data/views/index.erb +51 -0
- metadata +42 -5
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
img_cache/*
|
data/README.textile
CHANGED
@@ -1,20 +1,19 @@
|
|
1
|
-
|
1
|
+
Ruby gem to create meme images (images with text added at the top and bottom).
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
Also includes Sinatra app that exposes API over HTTP which is currently
|
4
|
+
running "http://memecaptain.com/":http://memecaptain.com/
|
5
|
+
|
6
|
+
Works with animated gifs.
|
6
7
|
|
7
8
|
<pre>
|
8
9
|
<code>
|
9
10
|
require 'meme_captain'
|
10
11
|
|
11
|
-
i = MemeCaptain.meme('luke300.jpeg', 'meme test', 'meme generator test')
|
12
|
-
i.display
|
13
|
-
|
14
12
|
require 'open-uri'
|
15
13
|
|
16
|
-
open('http://
|
14
|
+
open('http://image_from_web_or_local_file.jpg', 'rb') do |f|
|
17
15
|
i = MemeCaptain.meme(f, 'test', '1 2 3')
|
16
|
+
i.display
|
18
17
|
i.write('out.jpg')
|
19
18
|
end
|
20
19
|
</code>
|
data/config.ru
ADDED
File without changes
|
File without changes
|
data/lib/meme_captain.rb
CHANGED
@@ -1,40 +1,4 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
module_function
|
6
|
-
|
7
|
-
def meme(path_or_io, line1, line2, options={})
|
8
|
-
img = Magick::ImageList.new
|
9
|
-
if path_or_io.respond_to?(:read)
|
10
|
-
img.from_blob(path_or_io.read)
|
11
|
-
else
|
12
|
-
img.read(path_or_io)
|
13
|
-
end
|
14
|
-
|
15
|
-
options = {
|
16
|
-
:fill => 'white',
|
17
|
-
:font => 'Impact-Regular',
|
18
|
-
:gravity => Magick::CenterGravity,
|
19
|
-
:size => "#{img.columns}x#{img.rows / 4}",
|
20
|
-
:stroke => 'black',
|
21
|
-
:stroke_width => 1,
|
22
|
-
:background_color => 'none',
|
23
|
-
}.merge(options)
|
24
|
-
|
25
|
-
line1_caption = Magick::Image.read("caption:#{line1.upcase}") {
|
26
|
-
options.each { |k,v| self.send("#{k}=", v) }
|
27
|
-
}
|
28
|
-
|
29
|
-
line2_caption = Magick::Image.read("caption:#{line2.upcase}") {
|
30
|
-
options.each { |k,v| self.send("#{k}=", v) }
|
31
|
-
}
|
32
|
-
|
33
|
-
img[0].composite!(line1_caption[0], Magick::NorthGravity,
|
34
|
-
Magick::OverCompositeOp)
|
35
|
-
|
36
|
-
img[0].composite!(line2_caption[0], Magick::SouthGravity,
|
37
|
-
Magick::OverCompositeOp)
|
38
|
-
end
|
39
|
-
|
40
|
-
end
|
1
|
+
require 'meme_captain/content_type'
|
2
|
+
require 'meme_captain/meme'
|
3
|
+
require 'meme_captain/server'
|
4
|
+
require 'meme_captain/filesystem_cache'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module MemeCaptain
|
2
|
+
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# Determine content type from blob of image data.
|
6
|
+
def content_type(img_data)
|
7
|
+
if img_data[0,2].unpack('H4')[0] == 'ffd8'
|
8
|
+
'image/jpeg'
|
9
|
+
elsif img_data[0,8].unpack('H16')[0] == '89504e470d0a1a0a'
|
10
|
+
'image/png'
|
11
|
+
elsif %w{474946383961 474946383761}.include?(img_data[0,6].unpack('H12')[0])
|
12
|
+
'image/gif'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module MemeCaptain
|
2
|
+
|
3
|
+
# Cache data on filesystem.
|
4
|
+
class FilesystemCache
|
5
|
+
|
6
|
+
def initialize(root_dir); @root_dir = root_dir; end
|
7
|
+
|
8
|
+
# Get data from cache.
|
9
|
+
#
|
10
|
+
# On cache miss, if block given call block to get data from source and
|
11
|
+
# cache it. If no block given return nil on cache miss.
|
12
|
+
def get(id)
|
13
|
+
file_path = id_path(id)
|
14
|
+
|
15
|
+
if File.exist?(file_path)
|
16
|
+
open(file_path, 'rb') { |f| f.read }
|
17
|
+
else
|
18
|
+
put(id, yield) if block_given?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Put data in the cache and return it.
|
23
|
+
def put(id, data)
|
24
|
+
open(id_path(id), 'w') do |f|
|
25
|
+
f.flock(File::LOCK_EX)
|
26
|
+
f.write(data)
|
27
|
+
f.flock(File::LOCK_UN)
|
28
|
+
end
|
29
|
+
data
|
30
|
+
end
|
31
|
+
|
32
|
+
def id_path(id)
|
33
|
+
File.join(root_dir, File.expand_path(id, '/'))
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :root_dir
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'RMagick'
|
2
|
+
|
3
|
+
module MemeCaptain
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Create a meme image.
|
8
|
+
# Input can be an IO object or a blob of data.
|
9
|
+
def meme(input, line1, line2, options={})
|
10
|
+
img = Magick::ImageList.new
|
11
|
+
if input.respond_to?(:read)
|
12
|
+
img.from_blob(input.read)
|
13
|
+
else
|
14
|
+
img.from_blob(input)
|
15
|
+
end
|
16
|
+
|
17
|
+
options = {
|
18
|
+
:fill => 'white',
|
19
|
+
:font => 'Impact',
|
20
|
+
:gravity => Magick::CenterGravity,
|
21
|
+
:size => "#{img.columns * 1.8}x#{img.rows / 2}",
|
22
|
+
:stroke => 'black',
|
23
|
+
:stroke_width => 2,
|
24
|
+
:background_color => 'none',
|
25
|
+
}.merge(options)
|
26
|
+
|
27
|
+
line1_caption = Magick::Image.read("caption:#{line1.to_s.upcase}") {
|
28
|
+
options.each { |k,v| self.send("#{k}=", v) }
|
29
|
+
}
|
30
|
+
line1_caption[0].resize!(line1_caption[0].columns / 2,
|
31
|
+
line1_caption[0].rows / 2, Magick::LanczosFilter, 1.25)
|
32
|
+
|
33
|
+
line2_caption = Magick::Image.read("caption:#{line2.to_s.upcase}") {
|
34
|
+
options.each { |k,v| self.send("#{k}=", v) }
|
35
|
+
}
|
36
|
+
line2_caption[0].resize!(line2_caption[0].columns / 2,
|
37
|
+
line2_caption[0].rows / 2, Magick::LanczosFilter, 1.25)
|
38
|
+
|
39
|
+
img.each do |frame|
|
40
|
+
frame.composite!(line1_caption[0], Magick::NorthGravity,
|
41
|
+
Magick::OverCompositeOp)
|
42
|
+
|
43
|
+
frame.composite!(line2_caption[0], Magick::SouthGravity,
|
44
|
+
Magick::OverCompositeOp)
|
45
|
+
|
46
|
+
frame.strip!
|
47
|
+
end
|
48
|
+
img
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
require 'curb'
|
4
|
+
require 'sinatra/base'
|
5
|
+
|
6
|
+
require 'meme_captain'
|
7
|
+
|
8
|
+
module MemeCaptain
|
9
|
+
|
10
|
+
class Server < Sinatra::Base
|
11
|
+
|
12
|
+
get '/' do
|
13
|
+
@img_tag = if params[:u]
|
14
|
+
"<img src=\"#{h request.fullpath.sub(%r{^/}, '/i')}\" />"
|
15
|
+
else
|
16
|
+
''
|
17
|
+
end
|
18
|
+
|
19
|
+
@u = params[:u]
|
20
|
+
@tt= params[:tt]
|
21
|
+
@tb = params[:tb]
|
22
|
+
|
23
|
+
erb :index
|
24
|
+
end
|
25
|
+
|
26
|
+
get '/i' do
|
27
|
+
@processed_cache ||= MemeCaptain::FilesystemCache.new(
|
28
|
+
'img_cache/processed')
|
29
|
+
@source_cache ||= MemeCaptain::FilesystemCache.new('img_cache/source')
|
30
|
+
|
31
|
+
processed_id = Digest::SHA1.hexdigest(params.sort.map(&:join).join)
|
32
|
+
processed_img_data = @processed_cache.get(processed_id) {
|
33
|
+
source_id = Digest::SHA1.hexdigest(params[:u])
|
34
|
+
source_img_data = @source_cache.get(source_id) {
|
35
|
+
Curl::Easy.perform(params[:u]).body_str
|
36
|
+
}
|
37
|
+
MemeCaptain.meme(source_img_data, params[:tt], params[:tb]).to_blob {
|
38
|
+
self.quality = 100
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
headers = {
|
43
|
+
'Content-Type' => MemeCaptain.content_type(processed_img_data),
|
44
|
+
'ETag' => "\"#{Digest::SHA1.hexdigest(processed_img_data)}\"",
|
45
|
+
}
|
46
|
+
|
47
|
+
[ 200, headers, processed_img_data ]
|
48
|
+
end
|
49
|
+
|
50
|
+
helpers do
|
51
|
+
include Rack::Utils
|
52
|
+
alias_method :h, :escape_html
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
data/meme_captain.gemspec
CHANGED
@@ -4,7 +4,7 @@ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'meme_captain'
|
7
|
-
s.version = '0.0.
|
7
|
+
s.version = '0.0.2'
|
8
8
|
s.summary = 'create meme images'
|
9
9
|
s.description = s.summary
|
10
10
|
s.homepage = 'https://github.com/mmb/meme_captain'
|
@@ -14,7 +14,9 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.required_rubygems_version = '>= 1.3.6'
|
15
15
|
|
16
16
|
%w{
|
17
|
+
curb
|
17
18
|
rmagick
|
19
|
+
sinatra
|
18
20
|
}.each { |g| s.add_dependency g }
|
19
21
|
|
20
22
|
s.files = `git ls-files`.split("\n")
|
data/views/index.erb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
|
4
|
+
<head>
|
5
|
+
<title>Meme Captain</title>
|
6
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
7
|
+
</head>
|
8
|
+
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<p><a href="/">Meme Captain</a></p>
|
12
|
+
|
13
|
+
<p>Add text to images from the internet.</p>
|
14
|
+
|
15
|
+
<%= @img_tag %>
|
16
|
+
|
17
|
+
<form action="" method="get">
|
18
|
+
|
19
|
+
<table>
|
20
|
+
|
21
|
+
<tr>
|
22
|
+
<td><label for="u" />Source image URL: </label></td>
|
23
|
+
<td><input type="text" id="u" name="u" size="64" value="<%= h @u %>"/></td>
|
24
|
+
</tr>
|
25
|
+
|
26
|
+
<tr>
|
27
|
+
<td><label for="tt" />Top text: </label></td>
|
28
|
+
<td><input type="text" id="tt" name="tt" size="64" value="<%= h @tt %>" /></td>
|
29
|
+
</tr>
|
30
|
+
|
31
|
+
<tr>
|
32
|
+
<td><label for="tb" />Bottom text: </label></td>
|
33
|
+
<td><input type="text" id="tb" name="tb" size="64" value="<%= h @tb %>" /></td>
|
34
|
+
</tr>
|
35
|
+
|
36
|
+
<tr>
|
37
|
+
<td></td>
|
38
|
+
<td><input type="submit" value="Create Image" /></td>
|
39
|
+
</tr>
|
40
|
+
|
41
|
+
</table>
|
42
|
+
|
43
|
+
</form>
|
44
|
+
|
45
|
+
<p>by Matthew M. Boedicker <a href="mailto:matthewm@boedicker.org">matthewm@boedicker.org</a></p>
|
46
|
+
|
47
|
+
<p><a href="https://github.com/mmb/meme_captain">source code</a></p>
|
48
|
+
|
49
|
+
</body>
|
50
|
+
|
51
|
+
</html>
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: meme_captain
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Matthew M. Boedicker
|
@@ -15,11 +15,11 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-04-
|
18
|
+
date: 2011-04-20 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
name:
|
22
|
+
name: curb
|
23
23
|
prerelease: false
|
24
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
@@ -32,6 +32,34 @@ dependencies:
|
|
32
32
|
version: "0"
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rmagick
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: sinatra
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
35
63
|
description: create meme images
|
36
64
|
email:
|
37
65
|
- matthewm@boedicker.org
|
@@ -42,10 +70,19 @@ extensions: []
|
|
42
70
|
extra_rdoc_files: []
|
43
71
|
|
44
72
|
files:
|
73
|
+
- .gitignore
|
45
74
|
- COPYING
|
46
75
|
- README.textile
|
76
|
+
- config.ru
|
77
|
+
- img_cache/processed/.gitignore
|
78
|
+
- img_cache/source/.gitignore
|
47
79
|
- lib/meme_captain.rb
|
80
|
+
- lib/meme_captain/content_type.rb
|
81
|
+
- lib/meme_captain/filesystem_cache.rb
|
82
|
+
- lib/meme_captain/meme.rb
|
83
|
+
- lib/meme_captain/server.rb
|
48
84
|
- meme_captain.gemspec
|
85
|
+
- views/index.erb
|
49
86
|
has_rdoc: true
|
50
87
|
homepage: https://github.com/mmb/meme_captain
|
51
88
|
licenses: []
|