--- a/config.py Tue Feb 10 01:24:59 2009 +0200
+++ b/config.py Tue Feb 10 02:57:16 2009 +0200
@@ -13,23 +13,27 @@
# build relative paths
relpath = lambda path : os.path.join(os.path.dirname(__file__), path)
+### ###
+### Configuration ###
+### ###
+
# timezone to use for logs
-LOG_TIMEZONE = pytz.timezone('Europe/Helsinki')
+LOG_TIMEZONE = pytz.timezone('Europe/Helsinki')
# timestamp format for logfiles
-LOG_TIMESTAMP_FMT = '%H:%M:%S'
+LOG_TIMESTAMP_FMT = '%H:%M:%S'
# character set used for logfiles
-LOG_CHARSET = 'utf-8'
+LOG_CHARSET = 'utf-8'
# log filename format
-LOG_FILENAME_FMT = '%Y-%m-%d'
+LOG_FILENAME_FMT = '%Y-%m-%d'
# the log parser that we use
-LOG_PARSER = IrssiParser(LOG_TIMEZONE, LOG_TIMESTAMP_FMT)
+LOG_PARSER = IrssiParser(LOG_TIMEZONE, LOG_TIMESTAMP_FMT)
# the statically defined channel list
-LOG_CHANNELS = ChannelList([
+LOG_CHANNELS = ChannelList([
LogChannel('tycoon', "OFTC", "#tycoon",
LogDirectory(relpath('logs/tycoon'), LOG_TIMEZONE, LOG_PARSER, LOG_CHARSET, LOG_FILENAME_FMT)
),
@@ -40,27 +44,39 @@
])
# date format for URLs
-URL_DATE_FMT = '%Y-%m-%d'
+URL_DATE_FMT = '%Y-%m-%d'
# month name format
-MONTH_FMT = '%B %Y'
+MONTH_FMT = '%B %Y'
# timezone name format
-TIMEZONE_FMT = '%Z %z'
+TIMEZONE_FMT = '%Z %z'
+
+# TTF fonts to use for drawing images
+FORMATTER_IMAGE_FONTS = {
+ 'default': (None, "Ugly default font" ),
+ 'ttf-dejavu-mono': ("/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono.ttf", "DejaVu Sans Mono" ),
+ 'ttf-liberation-mono': ("/usr/share/fonts/truetype/ttf-liberation/LiberationMono-Regular.ttf", "Liberation Mono Regular" )
+}
# available formatters
-LOG_FORMATTERS = log_formatter.FORMATTERS
+LOG_FORMATTERS = {
+ 'irssi': IrssiFormatter,
+}
# default preferences
-PREF_TIME_FMT_DEFAULT = '%H:%M:%S'
-PREF_DATE_FMT_DEFAULT = '%Y-%m-%d'
-PREF_TIMEZONE_DEFAULT = pytz.utc
-PREF_FORMATTER_DEFAULT = IrssiFormatter
-PREF_COUNT_DEFAULT = 200
-PREF_COUNT_MAX = None
+PREF_TIME_FMT_DEFAULT = '%H:%M:%S'
+PREF_DATE_FMT_DEFAULT = '%Y-%m-%d'
+PREF_TIMEZONE_DEFAULT = pytz.utc
+PREF_FORMATTER_DEFAULT = IrssiFormatter
+PREF_COUNT_DEFAULT = 200
+PREF_COUNT_MAX = None
+PREF_IMAGE_FONT_DEFAULT = 'default'
+PREF_IMAGE_FONT_SIZE_DEFAULT = 10
+PREF_IMAGE_FONT_SIZE_MAX = 32
# search line count options
-SEARCH_LINE_COUNT_OPTIONS = (
+SEARCH_LINE_COUNT_OPTIONS = (
(50, 50),
(100, 100),
(200, 200),
--- a/handlers.py Tue Feb 10 01:24:59 2009 +0200
+++ b/handlers.py Tue Feb 10 02:57:16 2009 +0200
@@ -68,24 +68,44 @@
return http.Redirect(urls.channel.build(request, channel=channel))
@preferences.handler(prefs.formatter)
-def channel_last (request, channel, count, formatter) :
+def channel_last (request, channel, count, formatter, type=None) :
"""
The main channel view page, displaying the most recent lines
"""
-
+
# get latest events
lines = channel.source.get_latest(count)
-
- # lines
- lines = formatter.format_html(lines)
+
+ # we can render in various modes...
+ if not type :
+ # normal HTML
+ lines = formatter.format_html(lines)
- return templates.render_to_response("channel_last",
- req = request,
- prefs = request.prefs,
- channel = channel,
- count = count,
- lines = lines,
- )
+ return templates.render_to_response("channel_last",
+ req = request,
+ prefs = request.prefs,
+ channel = channel,
+ count = count,
+ lines = lines,
+ )
+
+ elif type == 'txt' :
+ # plaintext
+ lines = formatter.format_txt(lines)
+
+ # build data
+ data = '\n'.join(data for line, data in lines)
+
+ return http.Response(data, 'text/plain')
+
+ elif type == 'png' :
+ # PNG image
+ png_data = formatter.format_png(lines)
+
+ return http.Response(png_data, 'image/png', charset=None)
+
+ else :
+ raise http.ResponseError("Unrecognized type: %r" % (type, ))
@preferences.handler(prefs.formatter, prefs.timezone, prefs.count)
def channel_link (request, channel, timestamp, formatter, timezone, count) :
--- a/helpers.py Tue Feb 10 01:24:59 2009 +0200
+++ b/helpers.py Tue Feb 10 02:57:16 2009 +0200
@@ -187,4 +187,17 @@
"""
return max(values)
+
+ def select_options (self, key_values, selected_key) :
+ """
+ Render a series of <option> tags for <select>
+ """
+ return '\n'.join(
+ '\t<option%s%s>%s</option>' % (
+ ' value="%s"' % key if key != value else '',
+ ' selected="selected"' if key == selected_key else '',
+ value
+ ) for key, value in key_values
+ )
+
--- a/log_formatter.py Tue Feb 10 01:24:59 2009 +0200
+++ b/log_formatter.py Tue Feb 10 02:57:16 2009 +0200
@@ -5,6 +5,7 @@
import re, xml.sax.saxutils
from log_line import LogTypes
+from log_formatter_pil import PILImageFormatter
class LogFormatter (object) :
"""
@@ -21,13 +22,17 @@
# use a fixed-width font for HTML output
html_fixedwidth = True
- def __init__ (self, tz, timestamp_fmt="%H:%M:%S") :
+ def __init__ (self, tz, timestamp_fmt, img_ttf_path, img_font_size) :
"""
- Initialize to format timestamps with the given timezone and timestamp
+ Initialize to format timestamps with the given timezone and timestamp.
+
+ Use the given TTF font to render image text with the given size, if given, otherwise, a default one.
"""
self.tz = tz
self.timestamp_fmt = timestamp_fmt
+ self.img_ttf_path = img_ttf_path
+ self.img_font_size = img_font_size
def _format_line_text (self, line, template_dict, full_timestamp=False) :
"""
@@ -77,6 +82,11 @@
"""
abstract
+
+ def format_png (self, lines) :
+ """
+ Format as a PNG image, returning the binary PNG data
+ """
class BaseHTMLFormatter (object) :
"""
@@ -101,7 +111,7 @@
return self.URL_REGEXP.sub(_encode_url, line)
-class IrssiTextFormatter (LogFormatter) :
+class IrssiTextFormatter (PILImageFormatter, LogFormatter) :
"""
Implements format_txt for irssi-style output
"""
@@ -129,13 +139,13 @@
# parameters
html_fixedwidth = True
- def format_html (self, lines, full_timestamps=False) :
+ def format_html (self, lines, **kwargs) :
"""
Just uses format_txt, but processes links, etc
"""
# format using IrssiTextFormatter
- for line, txt in self.format_txt(lines, full_timestamps) :
+ for line, txt in self.format_txt(lines, **kwargs) :
# escape HTML
html = xml.sax.saxutils.escape(txt)
@@ -145,11 +155,6 @@
# yield
yield line, html
-# define formatters by name
-FORMATTERS = {
- 'irssi': IrssiFormatter,
-}
-
def by_name (name) :
"""
Lookup and return a class LogFormatter by name
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/log_formatter_pil.py Tue Feb 10 02:57:16 2009 +0200
@@ -0,0 +1,74 @@
+"""
+ Use of PIL to render the image formatting stuff
+"""
+
+from PIL import Image, ImageDraw, ImageFont
+
+from cStringIO import StringIO
+
+class PILImageFormatter (object) :
+ """
+ Implements the basic image-rendering operations on top of format_txt
+ """
+
+ # the font we load
+ font = None
+
+ def _load_font (self) :
+ """
+ Use the configured img_ttf_path for a TrueType font, or a default one
+ """
+
+ if self.font :
+ pass
+
+ elif self.img_ttf_path :
+ # load truetype with configured size
+ self.font = ImageFont.truetype(self.img_ttf_path, self.img_font_size)
+
+ else :
+ # default
+ self.font = ImageFont.load_default()
+
+ return self.font
+
+ def format_png (self, lines, **kwargs) :
+ # load font
+ font = self._load_font()
+
+ # build list of plain-text line data
+ lines = list(data for line, data in self.format_txt(lines, **kwargs))
+
+ # lines sizes
+ line_sizes = [font.getsize(line) for line in lines]
+
+ # figure out how wide/high the image will be
+ width = max(width for width, height in line_sizes)
+ height = sum(height for width, height in line_sizes)
+
+ # create new B/W image
+ img = Image.new('L', (width, height), 0xff)
+
+ # drawer
+ draw = ImageDraw.Draw(img)
+
+ # starting offset
+ offset_y = 0
+
+ # draw the lines
+ for line, (width, height) in zip(lines, line_sizes) :
+ # draw
+ draw.text((0, offset_y), line, font=font)
+
+ # next offset
+ offset_y += height
+
+ # output buffer
+ buf = StringIO()
+
+ # save
+ img.save(buf, 'png')
+
+ # return data
+ return buf.getvalue()
+
--- a/preferences.py Tue Feb 10 01:24:59 2009 +0200
+++ b/preferences.py Tue Feb 10 02:57:16 2009 +0200
@@ -244,6 +244,47 @@
return tz.zone
+class ImageFont (Preference) :
+ """
+ Font for ImageFormatter
+ """
+
+ # set name
+ name = 'image_font'
+
+ def __init__ (self, font_dict, default_name) :
+ """
+ Use the given { name: (path, title) } dict and default the given name
+ """
+
+ self.font_dict = font_dict
+ self.default = self.parse(default_name)
+
+ def parse (self, name) :
+ """
+ name -> (name, path, title)
+ """
+
+ path, title = self.font_dict[name]
+
+ return name, path, title
+
+ def build (self, font_info) :
+ """
+ (name, path, title) -> name
+ """
+
+ name, path, title = font_info
+
+ return name
+
+class ImageFontSize (urltree.URLIntegerType, Preference) :
+ # set name, default
+ name = 'image_font_size'
+ default = config.PREF_IMAGE_FONT_SIZE_DEFAULT
+
+ # XXX: params
+
class Formatter (Preference) :
"""
LogFormatter to use
@@ -276,10 +317,13 @@
def process (self, prefs, fmt_cls) :
"""
- class LogFormatter -> LogFormatter(tz, time_fmt)
+ class LogFormatter -> LogFormatter(tz, time_fmt, image_font.path)
"""
- return fmt_cls(prefs[timezone], prefs[time_format])
+ font_name, font_path, font_title = prefs[image_font]
+ font_size = prefs[image_font_size]
+
+ return fmt_cls(prefs[timezone], prefs[time_format], font_path, font_size)
class Count (urltree.URLIntegerType, Preference) :
"""
@@ -299,6 +343,8 @@
time_format = TimeFormat()
date_format = DateFormat()
timezone = Timezone()
+image_font = ImageFont(config.FORMATTER_IMAGE_FONTS, config.PREF_IMAGE_FONT_DEFAULT)
+image_font_size = ImageFontSize()
formatter = Formatter(config.LOG_FORMATTERS, config.PREF_FORMATTER_DEFAULT)
count = Count()
@@ -306,6 +352,8 @@
time_format,
date_format,
timezone,
+ image_font,
+ image_font_size,
formatter,
count,
])
--- a/templates/channel.tmpl Tue Feb 10 01:24:59 2009 +0200
+++ b/templates/channel.tmpl Tue Feb 10 02:57:16 2009 +0200
@@ -10,9 +10,7 @@
</li><li class="join-left">
<form action="${urls.channel_select.build(req)}" method="GET">
<select name="channel">
- % for ch in channel_list :
- <option value="${ch.id}"${' selected="selected"' if ch == channel else ''}>${ch.title}</option>
- % endfor
+ ${h.select_options(((ch.id, ch.title) for ch in channel_list), channel.id)}
</select><input type="submit" value="Go »" />
</form>
</li>
--- a/templates/channel_search.tmpl Tue Feb 10 01:24:59 2009 +0200
+++ b/templates/channel_search.tmpl Tue Feb 10 02:57:16 2009 +0200
@@ -10,9 +10,7 @@
<input type="submit" value="Search" />
Results/page: <select name="count">
- % for cc, cc_label in config.SEARCH_LINE_COUNT_OPTIONS :
- <option value="${cc if cc else ''}"${' selected="selected"' if cc == count else ''}>${cc_label}</option>
- % endfor
+ ${h.select_options(((cc, cc_label) for cc, cc_label in config.SEARCH_LINE_COUNT_OPTIONS), count)}
</select>
</form>
--- a/templates/inc_paginate.tmpl Tue Feb 10 01:24:59 2009 +0200
+++ b/templates/inc_paginate.tmpl Tue Feb 10 02:57:16 2009 +0200
@@ -22,6 +22,9 @@
% endif
</li>
% endfor
+ % if _more and not _last :
+ <li>…</li>
+ % endif
<li>
% if _more and _last :
<span>More »</span>
--- a/templates/preferences.tmpl Tue Feb 10 01:24:59 2009 +0200
+++ b/templates/preferences.tmpl Tue Feb 10 02:57:16 2009 +0200
@@ -9,9 +9,7 @@
<p>
<label for="timezone">Timezone:</label>
<select name="timezone">
- % for tz_name in timezones :
- <option${' selected="selected"' if tz_name == prefs['timezone'].zone else ''}>${tz_name}</option>
- % endfor
+ ${h.select_options(((tz_name, tz_name) for tz_name in timezones), prefs['timezone'])}
</select>
<span class="example">(${h.tz_name(prefs['timezone'])})</span>
</p>
@@ -36,9 +34,7 @@
<p>
<label for="formatter">Formatter:</label>
<select name="formatter">
- % for fmt_name, fmt in preferences.formatter.formatters.iteritems() :
- <option value="${fmt_name}">${fmt.title}</option>
- % endfor
+ ${h.select_options(((fmt_name, fmt.title) for fmt_name, fmt in preferences.formatter.formatters.iteritems()), prefs['formatter'])}
</select>
</p>
@@ -48,6 +44,23 @@
<span class="example">(Blank for infinite)</span>
</p>
</fieldset>
+
+ <fieldset>
+ <legend>Image Options</legend>
+
+ <p>
+ <label for="image_font">Font:</label>
+ <select name="image_font">
+ ${h.select_options(((name, title) for name, (path, title) in config.FORMATTER_IMAGE_FONTS.iteritems()), prefs['image_font'][0])}
+ </select>
+ </p>
+
+ <p>
+ <label for="image_font_size">Font size:</label>
+ <input type="text" name="image_font_size" value="${prefs['image_font_size']}" />
+ <span class="example">pt</span>
+ </p>
+ </fieldset>
<input type="submit" value="Save" />
</form>
--- a/urls.py Tue Feb 10 01:24:59 2009 +0200
+++ b/urls.py Tue Feb 10 02:57:16 2009 +0200
@@ -35,7 +35,7 @@
preferences = url('/preferences', handlers.preferences_ )
channel_select = url('/channel_select/?channel:cid', handlers.channel_select )
channel = url('/channels/{channel:cid}', handlers.channel_last, count=20 )
-channel_last = url('/channels/{channel:cid}/last/{count:int=100}', handlers.channel_last )
+channel_last = url('/channels/{channel:cid}/last/{count:int=100}/{type=}', handlers.channel_last )
channel_link = url('/channels/{channel:cid}/link/{timestamp:ts}', handlers.channel_link )
channel_calendar = url('/channels/{channel:cid}/calendar/{year:int=0}/{month:int=0}', handlers.channel_calendar )
channel_date = url('/channels/{channel:cid}/date/{date:date}/?page:int=1', handlers.channel_date )