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