meme_captain 0.0.8 → 0.0.9

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/.gitignore CHANGED
@@ -1 +1,2 @@
1
- img_cache/*
1
+ img_cache/*
2
+ *~
data/ChangeLog CHANGED
@@ -1,3 +1,36 @@
1
+ 0.0.9 2012-01-19
2
+
3
+ Design and markup improvements on the site.
4
+
5
+ Allow use of all type metrics when comparing caption choices.
6
+
7
+ Add ability to use the ids in the URLs of generated images to make
8
+ new images from the same source by using the id as the source image
9
+ URL (an example id is abcdef.jpg).
10
+
11
+ Add a 404 page to the site.
12
+
13
+ Deprecate concept of temporary URLs and permanent URLs for images
14
+ generated from the site.
15
+
16
+ Use MongoDB for the site datastore.
17
+
18
+ Add "memecaptain" executable for creating memes using the gem from the
19
+ command line.
20
+
21
+ Add a watermark to images created on the site.
22
+
23
+ Shrink large source images to a maximum of 800 pixels per side for
24
+ images created on the site.
25
+
26
+ Define local source images in JSON instead of in the template and load
27
+ them with Ajax.
28
+
29
+ Add Google Image Search to the site.
30
+
31
+ Add new source images to the site: all the things 2, cool story bro,
32
+ aw yeah, Boromir, Ned Stark.
33
+
1
34
  0.0.8
2
35
  2011-11-23
3
36
 
data/README.md CHANGED
@@ -47,6 +47,10 @@ Example:
47
47
  http://memecaptain.com/g?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&tt=sure+i%27ll+test&tb=the+api
48
48
  ```
49
49
 
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
+
50
54
  ```json
51
55
  {
52
56
  permUrl: "http://memecaptain.com/i?u=http%3A%2F%2Fmemecaptain.com%2Fyao_ming.jpg&tt=sure+i%27ll+test&tb=the+api"
data/bin/memecaptain ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mime/types'
4
+ require 'open-uri'
5
+ require 'optparse'
6
+
7
+ require 'meme_captain'
8
+
9
+ options = {}
10
+
11
+ option_parser = OptionParser.new do |opts|
12
+ opts.banner = <<-eos
13
+ Usage: memecaptain INPUT_IMAGE [OPTIONS]
14
+
15
+ INPUT_IMAGE can be a file path, URL or - for stdin.
16
+
17
+ eos
18
+
19
+ opts.on('-t', '--top-text TEXT', 'top text') do |text|
20
+ options[:top_text] = text
21
+ end
22
+
23
+ opts.on('-b', '--bottom-text TEXT', 'bottom text') do |text|
24
+ options[:bottom_text] = text
25
+ end
26
+
27
+ opts.on('-o', '--output PATH', 'output path (- for stdout)') do |path|
28
+ options[:output] = path
29
+ end
30
+
31
+ opts.on('-f', '--font FONT', 'font') do |font|
32
+ options[:font] = font
33
+ end
34
+ end
35
+
36
+ option_parser.parse!
37
+
38
+ unless ARGV.empty?
39
+ input = ARGV[0]
40
+
41
+ input_io = if input == '-'
42
+ $stdin
43
+ else
44
+ open input, 'rb'
45
+ end
46
+
47
+ meme_options = options.select do |k,v|
48
+ [
49
+ :font,
50
+ ].include? k
51
+ end
52
+
53
+ output_image = MemeCaptain.meme(
54
+ input_io, options[:top_text], options[:bottom_text], meme_options)
55
+
56
+ input_io.close
57
+
58
+ output = if options[:output]
59
+ options[:output] == '-' ? $stdout : options[:output]
60
+ else
61
+ "meme.#{MIME::Types[output_image.mime_type][0].extensions[0]}"
62
+ end
63
+
64
+ output_image.write(output) { self.quality = 100 }
65
+ else
66
+ $stderr.puts option_parser
67
+ exit 1
68
+ end
data/config.ru CHANGED
@@ -1,5 +1,7 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
2
 
3
+ require 'mongo'
4
+ require 'mongo_mapper'
3
5
  require 'rack'
4
6
 
5
7
  require 'meme_captain'
@@ -9,4 +11,15 @@ use Rack::Sendfile
9
11
 
10
12
  use Rack::Static, :urls => %w{/tmp}, :root => 'public'
11
13
 
14
+ MongoMapper.connection = Mongo::Connection.new
15
+ MongoMapper.database = 'memecaptain'
16
+
17
+ MemeCaptain::MemeData.ensure_index :meme_id
18
+
19
+ MemeCaptain::MemeData.ensure_index [
20
+ [:source_url, 1],
21
+ [:top_text, 1],
22
+ [:bottom_text, 1],
23
+ ]
24
+
12
25
  run MemeCaptain::Server
@@ -4,18 +4,24 @@ module MemeCaptain
4
4
  class CaptionChoice
5
5
  include Comparable
6
6
 
7
- def initialize(fits, pointsize, text)
8
- @fits = fits
7
+ def initialize(pointsize, metrics, text, bound_width, bound_height)
9
8
  @pointsize = pointsize
9
+ @metrics = metrics
10
10
  @text = text
11
+ @bound_width = bound_width
12
+ @bound_height = bound_height
11
13
  end
12
14
 
13
15
  def num_lines
14
16
  text.count("\n") + 1
15
17
  end
16
18
 
19
+ def fits
20
+ metrics.width <= bound_width and metrics.height <= bound_height
21
+ end
22
+
17
23
  def fits_i
18
- fits ? 1: 0
24
+ fits ? 1 : 0
19
25
  end
20
26
 
21
27
  def <=>(other)
@@ -24,10 +30,11 @@ module MemeCaptain
24
30
  other.fits ? -other.num_lines : other.num_lines]
25
31
  end
26
32
 
27
- attr_accessor :fits
28
33
  attr_accessor :pointsize
34
+ attr_accessor :metrics
29
35
  attr_accessor :text
30
-
36
+ attr_accessor :bound_width
37
+ attr_accessor :bound_height
31
38
  end
32
39
 
33
40
  end
@@ -6,20 +6,22 @@ module MemeCaptain
6
6
  # Calculate the largest pointsize for text that will be in a width x
7
7
  # height box.
8
8
  #
9
- # Return [pointsize, fits] where pointsize is the largest pointsize and
10
- # fits is true if that pointsize will fit in the box.
9
+ # Return [pointsize, metrics] where pointsize is the largest pointsize and
10
+ # metrics is the RMagick multiline type metrics of the best fit.
11
11
  def calc_pointsize(width, height, text, min_pointsize)
12
12
  current_pointsize = min_pointsize
13
13
 
14
- fits = false
14
+ metrics = nil
15
15
 
16
16
  loop {
17
17
  self.pointsize = current_pointsize
18
+ last_metrics = metrics
18
19
  metrics = get_multiline_type_metrics(text)
20
+
19
21
  if metrics.width > width or metrics.height > height
20
22
  if current_pointsize > min_pointsize
21
23
  current_pointsize -= 1
22
- fits = true
24
+ metrics = last_metrics
23
25
  end
24
26
  break
25
27
  else
@@ -27,7 +29,7 @@ module MemeCaptain
27
29
  end
28
30
  }
29
31
 
30
- [current_pointsize, fits]
32
+ [current_pointsize, metrics]
31
33
  end
32
34
 
33
35
  end
@@ -0,0 +1,49 @@
1
+ require 'digest/sha1'
2
+ require 'fileutils'
3
+ require 'mime/types'
4
+
5
+ module MemeCaptain
6
+
7
+ module ImageList
8
+
9
+ # Mix-in for Magick::ImageList to add saving to the filesystem based on a
10
+ # hash.
11
+ module Cache
12
+
13
+ # Get the extension for this image.
14
+ def extension
15
+ {
16
+ 'image/jpeg' => 'jpg',
17
+ }[mime_type] || MIME::Types[mime_type][0].extensions[0]
18
+ end
19
+
20
+ # Store this image in the filesystem and return its path.
21
+ def cache(hash_base, dir)
22
+ hashe = Digest::SHA1.hexdigest(hash_base)
23
+
24
+ cache_dir = File.join(dir, hashe[0,3])
25
+ FileUtils.mkdir_p cache_dir
26
+
27
+ file_part = hashe[3..-1]
28
+ fs_path = File.join(cache_dir, "#{file_part}.#{extension}")
29
+
30
+ # If there is a collision add 0's until the filename is unique.
31
+ zeroes = 0
32
+ while File.exist? fs_path
33
+ zeroes += 1
34
+ fs_path = File.join(cache_dir,
35
+ "#{file_part}#{'0' * zeroes}.#{extension}")
36
+ end
37
+
38
+ write(fs_path) {
39
+ self.quality = 100
40
+ }
41
+
42
+ fs_path
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,26 @@
1
+ require 'curb'
2
+
3
+ module MemeCaptain
4
+
5
+ module ImageList
6
+
7
+ # Mix-in for Magick::ImageList to add loading from a URL.
8
+ module Fetch
9
+
10
+ # Load this image from a URL.
11
+ def fetch!(url)
12
+ curl = Curl::Easy.perform(url) do |c|
13
+ c.useragent = 'Meme Captain http://memecaptain.com/'
14
+ end
15
+ unless curl.response_code == 200
16
+ raise "Error loading source image url #{url}"
17
+ end
18
+
19
+ from_blob curl.body_str
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,28 @@
1
+ require 'RMagick'
2
+
3
+ module MemeCaptain
4
+
5
+ module ImageList
6
+
7
+ # Source image for meme generation.
8
+ class SourceImage < Magick::ImageList
9
+ include Cache
10
+ include Fetch
11
+ include Watermark
12
+
13
+ # Shrink image if necessary and add watermark.
14
+ def prepare!(max_side, watermark_img)
15
+ if size == 1 and (columns > max_side or rows > max_side)
16
+ resize_to_fit! max_side
17
+ end
18
+
19
+ watermark_mc watermark_img
20
+
21
+ strip!
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'RMagick'
2
+
3
+ module MemeCaptain
4
+
5
+ module ImageList
6
+
7
+ # Mix-in for Magick::ImageList to add watermark.
8
+ module Watermark
9
+
10
+ # Watermark this image using another image.
11
+ def watermark_mc(watermark_img)
12
+ self.each do |frame|
13
+ frame.composite!(watermark_img, Magick::SouthEastGravity,
14
+ -frame.page.width + frame.columns + frame.page.x,
15
+ -frame.page.height + frame.rows + frame.page.y,
16
+ Magick::OverCompositeOp)
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,5 @@
1
+ require 'meme_captain/image_list/cache'
2
+ require 'meme_captain/image_list/fetch'
3
+ require 'meme_captain/image_list/watermark'
4
+
5
+ require 'meme_captain/image_list/source_image'
@@ -69,10 +69,11 @@ module MemeCaptain
69
69
  }.uniq
70
70
 
71
71
  choices = wrap_tries.map do |wrap_try|
72
- pointsize, fits = draw.calc_pointsize(
72
+ pointsize, metrics = draw.calc_pointsize(
73
73
  text_width, text_height, wrap_try, min_pointsize)
74
74
 
75
- CaptionChoice.new(fits, pointsize, wrap_try)
75
+ CaptionChoice.new(pointsize, metrics, wrap_try, text_width,
76
+ text_height)
76
77
  end
77
78
 
78
79
  choice = choices.max
@@ -0,0 +1,34 @@
1
+ require 'mongo_mapper'
2
+
3
+ module MemeCaptain
4
+
5
+ class MemeData
6
+ include MongoMapper::Document
7
+
8
+ set_collection_name 'meme'
9
+
10
+ key :meme_id, String
11
+ key :fs_path, String
12
+ key :mime_type, String
13
+ key :size, Integer
14
+
15
+ key :source_url, String
16
+ key :source_fs_path, String
17
+ key :top_text, String
18
+ key :bottom_text, String
19
+
20
+ key :request_count, Integer
21
+ key :last_request, Time
22
+
23
+ key :creator_ip, String
24
+
25
+ timestamps!
26
+
27
+ def requested!
28
+ increment :request_count => 1
29
+ set :last_request => Time.now
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -1,84 +1,167 @@
1
1
  require 'digest/sha1'
2
- require 'uri'
3
2
 
4
- require 'curb'
5
3
  require 'json'
4
+ require 'rack'
6
5
  require 'sinatra/base'
7
6
 
8
- require 'meme_captain'
9
-
10
7
  module MemeCaptain
11
8
 
12
9
  class Server < Sinatra::Base
13
10
 
14
- ImageExts = %w{.jpeg .gif .png}
15
-
16
- set :root, File.join(File.dirname(__FILE__), '..', '..')
11
+ set :root, File.expand_path(File.join('..', '..'), File.dirname(__FILE__))
12
+ set :source_img_max_side, 800
13
+ set :watermark, Magick::ImageList.new(File.expand_path(
14
+ File.join('..', '..', 'watermark.png'), File.dirname(__FILE__)))
17
15
 
18
16
  get '/' do
19
17
  @u = params[:u]
20
18
  @tt= params[:tt]
21
19
  @tb = params[:tb]
22
20
 
21
+ @root_url = url('/')
22
+
23
23
  erb :index
24
24
  end
25
25
 
26
- def gen(params)
27
- @processed_cache ||= MemeCaptain::FilesystemCache.new('public/tmp')
28
- @source_cache ||= MemeCaptain::FilesystemCache.new('img_cache/source')
26
+ def normalize_params(p)
27
+ result = {
28
+ 'u' => p[:u],
29
+ # convert to empty string if null
30
+ 'tt' => p[:tt].to_s,
31
+ 'tb' => p[:tb].to_s,
32
+ }
29
33
 
30
- processed_id = Digest::SHA1.hexdigest(params.sort.map(&:join).join)
31
- @processed_cache.get_path(processed_id, ImageExts) {
32
- source_id = Digest::SHA1.hexdigest(params[:u])
33
- source_img_data = @source_cache.get_data(source_id, ImageExts) {
34
- curl = Curl::Easy.perform(params[:u]) do |c|
35
- c.useragent = 'Meme Captain http://memecaptain.com/'
36
- end
37
- unless curl.response_code == 200
38
- raise "Error loading source image url #{params[:u]}"
39
- end
40
- curl.body_str
41
- }
34
+ # if the id of an existing meme is passed in as the source url, use the
35
+ # source image of that meme for the source image
36
+ if result['u'][%r{^[a-f0-9]+\.(?:gif|jpg|png)$}]
37
+ if existing_as_source = MemeData.find_by_meme_id(result['u'])
38
+ result['u'] = existing_as_source.source_url
39
+ end
40
+ end
41
+
42
+ # hash with string keys that can be accessed by symbol
43
+ Hash.new { |hash,key| hash[key.to_s] if Symbol === key }.merge(result)
44
+ end
42
45
 
43
- meme_img = MemeCaptain.meme(source_img_data, params[:tt], params[:tb])
44
- current_format = meme_img.format
46
+ def gen(p)
47
+ norm_params = normalize_params(p)
48
+
49
+ if existing = MemeData.first(
50
+ :source_url => norm_params[:u],
51
+ :top_text => norm_params[:tt],
52
+ :bottom_text => norm_params[:tb]
53
+ )
54
+ existing
55
+ else
56
+ if same_source = MemeData.find_by_source_url(norm_params[:u])
57
+ source_fs_path = same_source.source_fs_path
58
+ 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')
63
+ end
64
+
65
+ open(source_fs_path, 'rb') do |source_io|
66
+ meme_img = MemeCaptain.meme(source_io, norm_params[:tt],
67
+ norm_params[:tb])
68
+ meme_img.extend ImageList::Cache
45
69
 
46
- meme_img.to_blob {
47
- self.quality = 100
48
70
  # convert non-animated gifs to png
49
- if current_format == 'GIF' and meme_img.size == 1
50
- self.format = 'PNG'
71
+ if meme_img.format == 'GIF' and meme_img.size == 1
72
+ meme_img.format = 'PNG'
51
73
  end
52
- }
53
- }
74
+
75
+ params_s = norm_params.sort.map(&:join).join
76
+ meme_hash = Digest::SHA1.hexdigest(params_s)
77
+
78
+ meme_id = nil
79
+ (6..meme_hash.size).each do |len|
80
+ meme_id = "#{meme_hash[0,len]}.#{meme_img.extension}"
81
+ break unless MemeData.where(:meme_id => meme_id).count > 0
82
+ end
83
+
84
+ meme_fs_path = meme_img.cache(params_s, File.join('public', 'meme'))
85
+
86
+ meme_img.write(meme_fs_path) {
87
+ self.quality = 100
88
+ }
89
+
90
+ meme_data = MemeData.new(
91
+ :meme_id => meme_id,
92
+ :fs_path => meme_fs_path,
93
+ :mime_type => meme_img.mime_type,
94
+ :size => File.size(meme_fs_path),
95
+
96
+ :source_url => norm_params[:u],
97
+ :source_fs_path => source_fs_path,
98
+ :top_text => norm_params[:tt],
99
+ :bottom_text => norm_params[:tb],
100
+
101
+ :request_count => 0,
102
+
103
+ :creator_ip => request.ip
104
+ )
105
+
106
+ meme_data.save! :safe => true
107
+
108
+ meme_data
109
+ end
110
+
111
+ end
54
112
  end
55
113
 
56
114
  get '/g' do
115
+ raise Sinatra::NotFound if params[:u].to_s.empty?
116
+
57
117
  begin
58
- processed_cache_path = gen(params)
118
+ meme_data = gen(params)
59
119
 
60
- temp_url = URI(request.url)
61
- temp_url.path = processed_cache_path.sub('public', '')
62
- temp_url.query = nil
120
+ meme_url = url("/#{meme_data.meme_id}")
63
121
 
64
- perm_url = URI(request.url)
65
- perm_url.path = '/i'
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('&')
66
128
 
67
129
  [200, { 'Content-Type' => 'application/json' }, {
68
- 'tempUrl' => temp_url.to_s,
69
- 'permUrl' => perm_url.to_s,
130
+ 'tempUrl' => meme_url,
131
+ 'permUrl' => meme_url,
132
+ 'templateUrl' => url("/?#{template_query}"),
70
133
  }.to_json]
71
134
  rescue => error
72
135
  [500, { 'Content-Type' => 'text/plain' }, error.to_s]
73
136
  end
74
137
  end
75
138
 
139
+ def serve_img(meme_data)
140
+ meme_data.requested!
141
+
142
+ content_type meme_data.mime_type
143
+
144
+ FileBody.new meme_data.fs_path
145
+ end
146
+
76
147
  get '/i' do
77
- processed_cache_path = gen(params)
148
+ raise Sinatra::NotFound if params[:u].to_s.empty?
149
+
150
+ serve_img(gen(params))
151
+ end
152
+
153
+ get %r{^/([a-f0-9]+\.(?:gif|jpg|png))$} do
154
+ if meme_data = MemeData.find_by_meme_id(params[:captures][0])
155
+ serve_img meme_data
156
+ else
157
+ raise Sinatra::NotFound
158
+ end
159
+ end
78
160
 
79
- content_type MIME::Types.type_for(processed_cache_path)[0].to_s
161
+ not_found do
162
+ @root_url = url('/')
80
163
 
81
- MemeCaptain::FileBody.new(processed_cache_path)
164
+ erb :'404'
82
165
  end
83
166
 
84
167
  helpers do
@@ -1,3 +1,3 @@
1
1
  module MemeCaptain
2
- VERSION = '0.0.8'
2
+ VERSION = '0.0.9'
3
3
  end
data/lib/meme_captain.rb CHANGED
@@ -2,8 +2,8 @@ require 'meme_captain/caption'
2
2
  require 'meme_captain/caption_choice'
3
3
  require 'meme_captain/draw'
4
4
  require 'meme_captain/file_body'
5
- require 'meme_captain/filesystem_cache'
5
+ require 'meme_captain/image_list'
6
6
  require 'meme_captain/meme'
7
- require 'meme_captain/mime_type'
7
+ require 'meme_captain/meme_data'
8
8
  require 'meme_captain/server'
9
9
  require 'meme_captain/version'
data/meme_captain.gemspec CHANGED
@@ -15,13 +15,17 @@ Gem::Specification.new do |s|
15
15
  s.email = %w{matthewm@boedicker.org}
16
16
 
17
17
  %w{
18
+ bson_ext
18
19
  curb
19
20
  json
20
21
  mime-types
22
+ mongo
23
+ mongo_mapper
21
24
  rack
22
25
  rmagick
23
26
  sinatra
24
27
  }.each { |g| s.add_dependency g }
25
28
 
26
29
  s.files = `git ls-files`.split("\n")
30
+ s.executables = %w{memecaptain}
27
31
  end