forked from matclayton/appengine-thumbnails
/
main.py
executable file
·142 lines (116 loc) · 5.25 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext import db
from google.appengine.api.images import Image, JPEG, NotImageError
from google.appengine.api import memcache
from google.appengine.api import urlfetch
import urllib, random
from datetime import datetime, timedelta
BASE_URL = ''
ALLOWED_DIMENSIONS = (
(300, 300, 85),
)
class OriginalImage(db.Model):
image_data = db.BlobProperty()
class BaseResizeHandler(webapp.RequestHandler):
def get(self, width, height, quality, content_url):
width, quality = int(width), int(quality)
height = int(height) if height else None
if ALLOWED_DIMENSIONS and not (width, height, quality) in ALLOWED_DIMENSIONS:
return self.error('Disallowed dimensions.')
if not content_url:
return self.error('No content url given.')
if BASE_URL:
url = BASE_URL + content_url
else:
url = 'http://' + content_url
response_image_data = self.get_cached(url, width, height, quality)
if response_image_data is not None:
self.send_image_response(response_image_data)
return
image_data = self.load_image_data(url)
response_image_data = self.process_image(image_data, width, height, quality)
self.set_cached(url, width, height, quality, response_image_data)
self.send_image_response(response_image_data)
def get_cache_key(self, url, width, height, quality):
return 'resized-image:%s:%s:%s:%s:%s' % (self.cache_prefix, url, width, height, quality)
def get_cached(self, url, width, height, quality):
return memcache.get(self.get_cache_key(url, width, height, quality))
def set_cached(self, url, width, height, quality, image_data):
# Cache for 25 to 35 days, to avoid everything expiring at once
cache_days = 25 + (random.randint(0, 10))
return memcache.add(self.get_cache_key(url, width, height, quality), image_data, cache_days * 24 * 60 * 60)
def load_image_data(self, url):
key = 'fetched-image:%s' % url
image = OriginalImage.get_by_key_name(key)
if image is None:
response = urlfetch.fetch(url)
if response.status_code == 200:
image_data = response.content
image = OriginalImage(image_data=image_data, key_name=key)
image.put()
return image.image_data
def send_image_response(self, image_data):
self.response.headers['Content-Type'] = 'image/jpeg'
self.response.headers['Cache-Control'] = 'public,max-age=31536000'
expires_date = datetime.utcnow() + timedelta(days=365)
self.response.headers['Expires'] = expires_date.strftime("%d %b %Y %H:%M:%S GMT")
self.response.out.write(image_data)
def error(self, msg):
self.response.out.write("""
<html>
<head><title>Error</title></head>
<body><h1>%s</h1></body>
</html>""" % msg
)
class WidthHandler(BaseResizeHandler):
cache_prefix = 'width'
def get(self, width, quality, content_url):
super(WidthHandler, self).get(width, None, quality, content_url)
def process_image(self, image_data, width, height, quality):
img = Image(image_data=image_data)
try:
img.resize(width=width)
return img.execute_transforms(output_encoding=JPEG, quality=quality)
except NotImageError:
return image_data
class CropHandler(BaseResizeHandler):
cache_prefix = 'crop'
def process_image(self, image_data, width, height, quality):
img = Image(image_data = image_data)
image_ops = crop_ops(img.width, img.height, width, height)
if image_ops:
for op, kwargs in image_ops:
getattr(img, op)(**kwargs)
return img.execute_transforms(output_encoding = JPEG, quality=quality)
return image_data
def crop_ops(width, height, requested_width, requested_height):
#import logging
width, height = float(width), float(height)
requested_width, requested_height = float(requested_width), float(requested_height)
r = max(requested_width/width, requested_height/height)
#logging.info('r:%s' % r)
new_width, new_height = width*r, height*r
#logging.info('new_width:%s new_height:%s' % (new_width, new_height))
ops = []
if r != 1.0:
ops.append(('resize', {'width': int(new_width), 'height': int(new_height)}))
ex, ey = (new_width-min(new_width, requested_width))/2, (new_height-min(new_height, requested_height))/2
#logging.info('ex:%s ey:%s' % (ex, ey))
if ex or ey:
ops.append(
('crop', {
'left_x': ex / new_width,
'right_x': (new_width - ex) / new_width,
'top_y': ey / new_height,
'bottom_y': (new_height - ey) / new_height,
})
)
#ops.append(('resize', {'width': int(width), 'height': int(height)}))
#logging.info('ops:%s' % ops)
return ops
if __name__ == '__main__':
run_wsgi_app(webapp.WSGIApplication([
(r'/w/(\d+)/h/(\d+)/q/(\d+)/([a-zA-Z0-9-_/.:]+)', CropHandler),
(r'/w/(\d+)/q/(\d+)/([a-zA-Z0-9-_/.:]+)', WidthHandler),
], debug=False))