#!/usr/bin/python
# Copyright 2009 Tero Marttila
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import werkzeug
from werkzeug.exceptions import HTTPException
from PIL import Image, ImageDraw, ImageFont, ImageEnhance
from cStringIO import StringIO
import random, itertools, time, os.path
# monkeypatch 2.5 to add missing 2.6 features
if not hasattr(itertools, 'izip_longest') :
def izip_longest(*args, **kwds):
# izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
fillvalue = kwds.get('fillvalue')
def sentinel(counter = ([fillvalue]*(len(args)-1)).pop):
yield counter() # yields the fillvalue, or raises IndexError
fillers = itertools.repeat(fillvalue)
iters = [itertools.chain(it, sentinel(), fillers) for it in args]
try:
for tup in itertools.izip(*iters):
yield tup
except IndexError:
pass
itertools.izip_longest = izip_longest
class Defaults :
"""
Default values for parameters
"""
text_lang = 'en'
chars = [ u'"', u'!', u'?' ]
colors = [
"#0469af",
"#fbc614",
"#e1313b",
]
font_name = 'helvetica'
font_size = 30
bg_color = "#ffffff"
line_spacing = -10
sharpness = 0.6
img_format = 'png'
TEXT_BY_LANG = dict(
en = [
u"aalto",
u"unive",
u"rsity"
],
fi = [
u"aalto",
u"yliop",
u"isto"
],
se = [
u"aalto",
u"univer",
u"sitetet",
],
)
STATIC_PATH = "static"
FONTS = {
'dejavu-sans-bold': "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans-Bold.ttf",
'helvetica': "fonts/HELR65W.TTF",
}
IMAGE_FORMATS = {
'jpeg': 'jpeg',
'png': 'png',
'bmp': 'bmp'
}
FONT_SIZE_MAX = 1024
IMG_SIZE_MAX = 1024
TILE_SIZE = (100, 100)
SITE_URL = "http://qmsk.net/stuff/aaltologo/"
SOURCE_URL = "http://hg.qmsk.net/aaltologotin"
# enable debugging
DEBUG = True
def randomize (seq) :
"""
Returns the given sequence in random order as a list
"""
# copy
l = list(seq)
# rearrange
random.shuffle(l)
return l
def randomize_str_char (str) :
"""
Randomize the given string by moving one char around
"""
l = list(str)
c = l.pop(random.randint(0, len(l) - 1))
l.insert(random.randint(0, len(l)), c)
return ''.join(l)
def build_data (text, chars, line_colors, random_chars=True, random_text=False, random_text_char=False) :
"""
Returns a matrix of (text, color) tuples representing the data to render
[ [ (str, str) ] ]
text - list of lines
chars - list of random chars to interpse
line_colors - list of colors to draw the chars in
random_chars - randomize the lines the chars go in
random_text - randomize the chars in each line
random_text_char - randomize each line by moving one char around
"""
data = []
# if no chars given, don't insert any
if not chars :
chars = []
# randomize char order across lines?
if random_chars :
chars = randomize(chars)
for line, char, color in itertools.izip_longest(text, chars, line_colors, fillvalue=None) :
if not line :
continue
# pick position to place char
if len(line) >= 2 :
pos = random.randint(1, len(line) - 1)
else :
pos = random.randint(0, 1)
# default color
if not color :
color = "#000000"
# randomize text in some way?
if random_text :
line = ''.join(randomize(line))
if random_text_char :
line = randomize_str_char(line)
# split into three parts
if char :
data.append([
(line[:pos], "#000000"),
(char, color),
(line[pos:], "#000000"),
])
else :
data.append([
(line, "#000000"),
])
return data
def load_font (font_name, font_size) :
"""
Load a font by name
"""
# load font
font_path = FONTS[font_name]
font = ImageFont.truetype(font_path, font_size)
return font
def render_img (data, font, background_color="#ffffff", line_spacing=0, img_size=None) :
"""
Render the data (as from build_data) as an image, using the given PIL.ImageFont, and return the PIL Image object
"""
img_width = img_height = 0
img_data = []
# compute image/segment width/height
for segments in data :
line_width = line_height = 0
# build a new list of segments with additional info
line_segments = []
for seg_text, seg_color in segments :
# compute rendered text size
seg_width, seg_height = font.getsize(seg_text)
# update line_*
line_width += seg_width
line_height = max(line_height, seg_height)
# build the new segments list
line_segments.append((seg_text, seg_color, seg_width))
# update img_*
img_width = max(img_width, line_width)
img_height += line_height
img_data.append((line_segments, line_height))
if img_size :
# override size
img_width, img_height = img_size
else :
# calculate height needed for line spacing
img_height += (len(img_data) - 1) * line_spacing
# create image
img = Image.new("RGB", (img_width, img_height), background_color)
draw = ImageDraw.Draw(img)
# draw text
img_y = 0
for segments, line_height in img_data :
img_x = 0
# draw each segment build above, incremeing along img_x
for seg_text, seg_color, seg_width in segments :
draw.text((img_x, img_y), seg_text, font=font, fill=seg_color)
img_x += seg_width
img_y += line_height + line_spacing
return img
def effect_sharpness (img, factor) :
"""
Sharpen the image by the given factor
"""
return ImageEnhance.Sharpness(img).enhance(factor)
def build_img (img, format='png') :
"""
Write the given PIL.Image as a string, returning the raw binary data
Format should be one of the PIL-supported image foarts
"""
# render PNG output
buf = StringIO()
img.save(buf, format)
data = buf.getvalue()
return data
class OptionType (object) :
def parse (self, val) :
"""
Unicode value -> object
"""
abstract
def build (self, val) :
"""
object -> unicode value
"""
return unicode(val)
def input (self, val) :
"""
HTML input item
"""
abstract
class StringOption (OptionType) :
def parse (self, val) :
return unicode(val)
def build (self, val) :
if val is None :
return ""
else :
return val
def input (self, opt, val) :
return """<input type="text" name="%(name)s" id="%(name)s" value="%(value)s" />""" % dict(
name = opt.name,
value = self.build(val),
)
def select (self, opt, val) :
return """<select name="%(name)s">
%(options)s
</select>""" % dict(
name = opt.name,
options = '\n'.join(
"\t<option %(selected)s>%(value)s</option>" % dict(
value = self.build(optval),
selected = 'selected="selected"' if val == optval else "",
) for optval in opt.range
),
)
class BoolOption (OptionType) :
def parse (self, val) :
if val.lower() in ('true', 't', '1', 'yes', 'y') :
return True
elif val.lower() in ('false', 'f', '0', 'no', 'n') :
return False
else :
raise ValueError(val)
def input (self, opt, val) :
return """<input type="checkbox" name="%(name)s" id="%(name)s" %(checked)s/>""" % dict(
name = opt.name,
checked = 'checked="checked"' if val else '',
)
class IntOption (StringOption) :
def parse (self, val) :
return int(val)
class FloatOption (StringOption) :
def parse (self, val) :
return float(val)
class ColorOption (StringOption) :
def _parse (self, val) :
if val.startswith('#') :
int(val[1:], 16)
return val
else :
raise ValueError(val)
class Option (object) :
def __init__ (self, name, is_list, type, default, range) :
self.name = name
self.is_list = is_list
self.type = type
self.default = default
self.range = range
def parse (self, args, force_bool=False) :
if self.is_list :
if self.name in args :
l = args.getlist(self.name, self.type.parse)
# special-case to handle a single param with a newline-separtated list
if len(l) == 1 :
if not l[0] :
return None
else :
return l[0].splitlines() # ('\r\n')
else :
return l
else :
return self.default
else :
if isinstance(self.type, BoolOption) and force_bool :
return self.name in args
elif isinstance(self.type, BoolOption) and not self.default and self.name in args :
return True
else :
return args.get(self.name, self.default, self.type.parse)
def build_list (self, value) :
if self.is_list and value :
return [self.type.build(val) for val in value]
else :
return [self.type.build(value)]
def _build_input (self, value) :
if self.is_list :
return """\
<textarea name="%(name)s" cols="30">\
%(data)s\
</textarea>""" % dict(
name = self.name,
data = '\n'.join(self.type.build(val) for val in value) if value else '',
)
elif self.range :
return self.type.select(self, value)
else :
return self.type.input(self, value)
def build_form (self, opts) :
value = opts[self.name]
return """\
<div class="param"><label for="%(name)s">%(title)s</label>%(input)s</div>\
""" % dict(
name = self.name,
title = self.name.title().replace('-', ' '),
input = self._build_input(value)
)
class Options (object) :
def __init__ (self, *options) :
self.options = list(options)
self.options_by_name = dict((opt.name, opt) for opt in options)
def parse (self, args, **kwargs) :
return dict((opt.name, opt.parse(args, **kwargs)) for opt in self.options)
OPTIONS = Options(
Option('lang', False, StringOption(), Defaults.text_lang, TEXT_BY_LANG.keys()),
Option('text', True, StringOption(), None, None),
Option('random-text', False, BoolOption(), False, None),
Option('random-text-char',False,BoolOption(), False, None),
Option('chars', True, StringOption(), Defaults.chars, None),
Option('random-chars', False, BoolOption(), True, None),
Option('colors', True, ColorOption(), Defaults.colors, None),
Option('font', False, StringOption(), Defaults.font_name, FONTS.keys()),
Option('font-size', False, IntOption(), Defaults.font_size, None),
Option('bg-color', False, ColorOption(), Defaults.bg_color, None),
Option('line-spacing', False, IntOption(), Defaults.line_spacing, None),
Option('sharpness', False, FloatOption(), Defaults.sharpness, None),
Option('image-format', False, StringOption(), Defaults.img_format, IMAGE_FORMATS.keys()),
Option('seed', False, IntOption(), None, None),
Option('img-width', False, IntOption(), None, None),
Option('img-height', False, IntOption(), None, None),
)
def handle_generic (req, img_size=None) :
# parse options
opts = OPTIONS.parse(req.args)
# postprocess
if opts['text'] is None :
opts['text'] = TEXT_BY_LANG[opts['lang']]
if opts['font-size'] > FONT_SIZE_MAX :
raise ValueError(opts['font-size'])
if opts['seed'] is None :
opts['seed'] = time.time()
if opts['img-width'] and opts['img-height'] :
img_size = (opts['img-width'], opts['img-height'])
if opts['img-width'] > IMG_SIZE_MAX or opts['img-height'] > IMG_SIZE_MAX :
raise ValueError(img_size)
# load/prep resources
random.seed(opts['seed'])
data = build_data(opts['text'], opts['chars'], opts['colors'], opts['random-chars'], opts['random-text'], opts['random-text-char'])
font = load_font(opts['font'], opts['font-size'])
# render the image
img = render_img(data, font, opts['bg-color'], opts['line-spacing'], img_size)
img = effect_sharpness(img, opts['sharpness'])
png_data = build_img(img, opts['image-format'])
# build the response
response = werkzeug.Response(png_data, mimetype='image/%s' % opts['image-format'])
return response
def handle_help (req) :
return werkzeug.Response('\n'.join(
"%-15s %4s %-10s %-20s %s" % data for data in [
("name", "", "type", "default", "range"),
("", "", "", "", ""),
] + [(
opt.name,
'list' if opt.is_list else 'item',
opt.type.__name__,
repr(opt.default),
opt.range if opt.range else ""
) for opt in OPTIONS.options]
), mimetype='text/plain')
def handle_logo (req) :
if 'help' in req.args :
return handle_help(req)
return handle_generic(req)
def handle_tile (req) :
return handle_generic(req, img_size=TILE_SIZE)
def handle_index (options, req) :
# parse options, force booleans if any form data was submitted, as checkboxes work that way
opts = options.parse(req.values, force_bool=bool(req.form))
# build query string of req things
qargs = [
# XXX: eek, this is weird
(opt.name, val)
for opt, vals in (
(options.options_by_name[opt_name], vals) for opt_name, vals in opts.iteritems()
) if vals != opt.default
# unpack (a, [b, c]) -> (a, b), (a, c)
for val in opt.build_list(vals)
]
img_url = req.url_root + "logo" + ("?%s" % werkzeug.url_encode(qargs) if qargs else '')
return werkzeug.Response("""\
<html>
<head>
<title>Aaltologotin</title>
<style type="text/css">
div#logo {
text-align: center;
padding: 100px;
}
img {
border: none;
}
div#info {
font-size: small;
padding-left: 25px;
margin-top: 50px;
color: grey;
}
label {
display: block;
float: left;
width: 150px;
}
div.param {
padding: 3px;
}
div#footer {
text-align: center;
font-size: x-small;
font-style: italic;
}
</style>
</head>
<body>
<div id='logo'>
<a href="%(img_url)s"><img src="%(img_url)s" alt="Aaltologo" /></a>
</div>
<div id='info'>
<h1>Aaltologotin</h1>
<p>Aaltologotin pulauttaa sinulle uuden, sattumanvaraisesti valitun aalto-logon!</p>
<form action="%(script_url)s" method="POST">
<fieldset>
<legend>Aalto-parameterit</legend>
%(form_fields)s
<input type="submit" value="Logota!" />
</fieldset>
</form>
</div>
<div id='footer'>
<a href="%(site_url)s">Aaltologotin</a> © 2009 Tero Marttila :: GPL <a href="%(source_url)s">Source code</a>
</div>
</body>
</html>""" % dict(
img_url = img_url,
script_url = req.url_root,
form_fields = "\n".join(
"\t%s" % opt.build_form(opts) for opt in options.options
),
site_url = SITE_URL,
source_url = SOURCE_URL,
), mimetype='text/html')
def handle_request (req) :
if req.path == '/' :
return handle_index(OPTIONS, req)
elif req.path.startswith('/logo') :
return handle_logo(req)
elif req.path == '/tile' :
return handle_tile(req)
else :
raise ValueError(req.path)
@werkzeug.Request.application
def wsgi_application (request) :
"""
Our werkzeug WSGI handler
"""
try :
# request -> response
response = handle_request(request)
return response
except HTTPException, e :
# return as HTTP response
return e
def build_app () :
"""
Build and return a WSGI application
"""
app = wsgi_application
# add other middleware
app = werkzeug.SharedDataMiddleware(app, {
'/static': STATIC_PATH,
})
if DEBUG :
# enable debugging
app = werkzeug.DebuggedApplication(app, evalex=False)
return app
def main_cgi (app) :
import wsgiref.handlers
handler = wsgiref.handlers.CGIHandler()
handler.run(app)
def main_fastcgi (app) :
import flup.server.fcgi
server = flup.server.fcgi.WSGIServer(app,
multithreaded = False,
multiprocess = False,
multiplexed = False,
bindAddress = None,
umask = None,
debug = True,
)
server.run()
def main () :
from sys import argv
# get our file extension
root, ext = os.path.splitext(argv[0])
# get our handler
handler = {
'.cgi': main_cgi,
'.fcgi': main_fastcgi,
}[ext]
# get our app
app = build_app()
# run
handler(app)
if __name__ == '__main__' :
main()