Class: Rack::GridThumb

Inherits:
Object
  • Object
show all
Defined in:
lib/rack_grid_thumb.rb

Constant Summary collapse

RE_TH_BASE =
/_([0-9]+x|x[0-9]+|[0-9]+x[0-9]+)(-(?:nw|n|ne|w|c|e|sw|s|se))?/
RE_TH_EXT =
/(\.(?:jpg|jpeg|png|gif))/i
TH_GRAV =
{
  '-nw' => :northwest,
  '-n' => :north,
  '-ne' => :northeast,
  '-w' => :west,
  '-c' => :center,
  '-e' => :east,
  '-sw' => :southwest,
  '-s' => :south,
  '-se' => :southeast
}

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ GridThumb

Returns a new instance of GridThumb.



23
24
25
26
27
28
# File 'lib/rack_grid_thumb.rb', line 23

def initialize(app, options={})
  @app = app
  @keylen = options[:keylength]
  @secret = options[:secret]
  @route  = generate_route(options[:prefix])
end

Instance Method Details

#_call(env) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/rack_grid_thumb.rb', line 43

def _call(env)
  response = catch(:halt) do
    throw :halt unless %w{GET HEAD}.include? env["REQUEST_METHOD"]
    @env = env
    @path = env["PATH_INFO"]
    if match = @path.match(@route)
      @source, dim, grav = extract_meta(match)
      @image = get_source_image
      @thumb = render_thumbnail(dim, grav) unless head?
      serve
    end
    nil
  end

  response || @app.call(env)
end

#bad_requestObject



166
167
168
169
170
171
# File 'lib/rack_grid_thumb.rb', line 166

def bad_request
  body = "Bad thumbnail parameters in #{@path}\n"
  [400, {"Content-Type" => "text/plain",
     "Content-Length" => body.size.to_s},
   [body]]
end

#call(env) ⇒ Object



39
40
41
# File 'lib/rack_grid_thumb.rb', line 39

def call(env)
  dup._call(env)
end

#create_tempfileObject

Creates a new tempfile



162
163
164
# File 'lib/rack_grid_thumb.rb', line 162

def create_tempfile
  Tempfile.new(::File.basename(@path)).tap { |f| f.close }
end

#eachObject



177
178
179
180
181
182
183
# File 'lib/rack_grid_thumb.rb', line 177

def each
  ::File.open(@thumb.path, "rb") { |file|
    while part = file.read(8192)
      yield part
    end
  }
end

#extract_meta(match) ⇒ Object

Extracts filename and options from the path.



61
62
63
64
65
66
67
68
69
70
# File 'lib/rack_grid_thumb.rb', line 61

def extract_meta(match)
  result = if @keylen
    extract_signed_meta(match)
  else
    extract_unsigned_meta(match)
  end

  throw :halt unless result
  result
end

#extract_signed_meta(match) ⇒ Object

Extracts filename and options from a signed path.



73
74
75
76
77
78
# File 'lib/rack_grid_thumb.rb', line 73

def extract_signed_meta(match)
  base, dim, grav, sig, ext = match.captures
  digest = Digest::SHA1.hexdigest("#{base}_#{dim}#{grav}#{ext}#{@secret}")[0..@keylen-1]
  throw(:halt, bad_request) unless sig && (sig == digest)
  [base + ext, dim, grav]
end

#extract_unsigned_meta(match) ⇒ Object

Extracts filename and options from an unsigned path.



81
82
83
84
# File 'lib/rack_grid_thumb.rb', line 81

def extract_unsigned_meta(match)
  base, dim, grav, ext = match.captures
  [base + ext, dim, grav]
end

#generate_route(prefix = nil) ⇒ Object

Generates route given a prefixes.



31
32
33
34
35
36
37
# File 'lib/rack_grid_thumb.rb', line 31

def generate_route(prefix = nil)
  if @keylen
    /^(\/#{prefix}\/\w+).*#{RE_TH_BASE}-([0-9a-f]{#{@keylen}})#{RE_TH_EXT}$/
  else
    /^(\/#{prefix}\/\w+).*#{RE_TH_BASE}#{RE_TH_EXT}$/
  end
end

#get_source_imageObject

Fetch the source image from the downstream app, returning the downstream app’s response if it is not a success.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/rack_grid_thumb.rb', line 88

def get_source_image
  status, headers, body = @app.call(@env.merge(
    "PATH_INFO" => @source
  ))
  unless (status >= 200 && status < 300) &&
      (headers["Content-Type"].split("/").first == "image")
    throw :halt, [status, headers, body]
  end

  @source_headers = headers

  if !head?
    if body.respond_to?(:path)
      ::File.open(body.path, 'rb')
    elsif body.respond_to?(:each)
      data = ''
      body.each { |part| data << part.to_s }
      Tempfile.new(::File.basename(@path)).tap do |f|
        f.binmode
        f.write(data)
        f.close
      end
    end
  else
    nil
  end
end

#head?Boolean

Returns:

  • (Boolean)


173
174
175
# File 'lib/rack_grid_thumb.rb', line 173

def head?
  @env["REQUEST_METHOD"] == "HEAD"
end

#parse_dimensions(meta) ⇒ Object

Parses the rendering options; returns false if rendering options are invalid



148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/rack_grid_thumb.rb', line 148

def parse_dimensions(meta)
  dimensions = meta.split('x').map do |dim|
    if dim.empty?
      nil
    elsif dim[0].to_i == 0
      throw :halt, bad_request
    else
      dim.to_i
    end
  end
  dimensions.any? ? dimensions : throw(:halt, bad_request)
end

#render_thumbnail(dim, grav) ⇒ Object

Renders a thumbnail from the source image. Returns a Tempfile.



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/rack_grid_thumb.rb', line 117

def render_thumbnail(dim, grav)
  gravity = grav ? TH_GRAV[grav] : :center
  width, height = parse_dimensions(dim)
  origin_width, origin_height = Mapel.info(@image.path)[:dimensions]
  width = [width, origin_width].min if width
  height = [height, origin_height].min if height
  output = create_tempfile
  cmd = Mapel(@image.path).gravity(gravity)
  if width && height
    cmd.resize!(width, height)
  else
    cmd.resize(width, height, 0, 0, '>')
  end
  cmd.to(output.path).run
  output
end

#serveObject

Serves the thumbnail. If this is a HEAD request we strip the body as well as the content length because the render was never run.



136
137
138
139
140
141
142
143
144
145
# File 'lib/rack_grid_thumb.rb', line 136

def serve
  response = if head?
    @source_headers.delete("Content-Length")
    [200, @source_headers, []]
  else
    [200, @source_headers.merge("Content-Length" => ::File.size(@thumb.path).to_s), self]
  end

  throw :halt, response
end

#to_pathObject



185
186
187
# File 'lib/rack_grid_thumb.rb', line 185

def to_path
  @thumb.path
end