image formatting \o/
authorTero Marttila <terom@fixme.fi>
Tue, 10 Feb 2009 02:57:16 +0200
changeset 79 43ac75054d5c
parent 78 85345abbd46a
child 80 a0662cff1d9d
image formatting \o/
config.py
handlers.py
helpers.py
log_formatter.py
log_formatter_pil.py
preferences.py
templates/channel.tmpl
templates/channel_search.tmpl
templates/inc_paginate.tmpl
templates/preferences.tmpl
urls.py
--- 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 &raquo;" />
         </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>&hellip;</li>
+        % endif
             <li>
             % if _more and _last :
                 <span>More &raquo;</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                       )