Browse Source

[pelican_comment_system] Added Avatars and Identicons

Bernhard Scheirle 11 years ago
parent
commit
98e2f16059

+ 20 - 183
pelican_comment_system/Readme.md

@@ -1,192 +1,29 @@
 # Pelican comment system
-The pelican comment system allows you to add static comments to your articles. It also supports replies to comments.
-
+The pelican comment system allows you to add static comments to your articles.
 The comments are stored in Markdown files. Each comment in it own file.
 
-See it in action here: [blog.scheirle.de](http://blog.scheirle.de/posts/2014/March/29/static-comments-via-email/)
+#### Features
+ - Static comments for each article
+ - Replies to comments
+ - Avatars and [Identicons](https://en.wikipedia.org/wiki/Identicon)
+ - Easy styleable via the themes
 
-Thanks to jesrui the author of [Static comments](https://github.com/getpelican/pelican-plugins/tree/master/static_comments). I reused some code from it.
+
+See it in action here: [blog.scheirle.de](http://blog.scheirle.de/posts/2014/March/29/static-comments-via-email/)
 
 Author             | Website                   | Github
 -------------------|---------------------------|------------------------------
 Bernhard Scheirle  | <http://blog.scheirle.de> | <https://github.com/Scheirle>
 
-## Installation
-Activate the plugin by adding it to your `pelicanconf.py`
-
-	PLUGIN_PATH = '/path/to/pelican-plugins'
-	PLUGINS = ['pelican_comment_system']
-	PELICAN_COMMENT_SYSTEM = True
-
-And modify your `article.html` theme (see below).
-
-## Settings
-Name                         | Type      | Default    | Description
------------------------------|-----------|------------|-------
-`PELICAN_COMMENT_SYSTEM`     | `boolean` | `False`    | Activates or deactivates the comment system
-`PELICAN_COMMENT_SYSTEM_DIR` | `string`  | `comments` | Folder where the comments are stored
-
-
-### Folder structure
-Every comment file has to be stored in a sub folder of `PELICAN_COMMENT_SYSTEM_DIR`.
-Sub folders are named after the `slug` of the articles.
-
-So the comments to your `foo-bar` article are stored in `comments/foo-bar/`
-
-The filenames of the comment files are up to you. But the filename is the Identifier of the comment (without extension).
-
-##### Example folder structure
-
-	.
-	└── comments
-		└── foo-bar
-		│   ├── 1.md
-		│   └── 0.md
-		└── some-other-slug
-			├── random-Name.md
-			├── 1.md
-			└── 0.md
-
-
-### Comment file
-##### Meta information
-Tag           | Required  | Description
---------------|-----------|----------------
-`date`        | yes       | Date when the comment was posted
-`replyto`     | no        | Identifier of the parent comment. Identifier = Filename (without extension)
-`locale_date` | forbidden | Will be overwritten with a locale representation of the date
-
-Every other (custom) tag gets parsed as well and will be available through the theme.
-
-
-##### Example of a comment file
-
-	date: 2014-3-21 15:02
-	author: Author of the comment
-	website: http://authors.website.com
-	replyto: 7
-	anothermetatag: some random tag
-
-	Content of the comment.
-
-### Theme
-In the `article.html` theme file are now two more variables available.
-
-Variables                         | Description
-----------------------------------|--------------------------
-`article.metadata.comments_count` | Amount of total comments for this article (including replies to comments)
-`article.metadata.comments`       | Array containing the top level comments for this article (no replies to comments)
-
-#### Comment object
-Variables  | Description
------------|--------------------------
-`id`       | Identifier of this comment
-`content`  | Content of this comment
-`metadata` | All metadata as in the comment file (or described above)
-`replies`  | Array containing the top level replies for this comment
-
-##### Example article.html theme
-(only the comment section)
-
-```html
-{% if article.metadata.comments %}
-	{% for comment in article.metadata.comments recursive %}
-		{% set metadata = comment.metadata %}
-		{% if loop.depth0 == 0 %}
-			{% set marginLeft = 0 %}
-		{% else %}
-			{% set marginLeft = 50 %}
-		{% endif %}
-			<article id="comment-{{comment.id}}" style="border: 1px solid #DDDDDD; padding: 5px 0px 0px 5px; margin: 0px -1px 5px {{marginLeft}}px;">
-				<a href="{{ SITEURL }}/{{ article.url }}#comment-{{comment.id}}" rel="bookmark" title="Permalink to this comment">Permalink</a>
-				<h4>{{ metadata['author'] }}</h4>
-				<p>Posted on <abbr class="published" title="{{ metadata['date'].isoformat() }}">{{ metadata['locale_date'] }}</abbr></p>
-
-				{{ comment.content }}
-				{% if comment.replies %}
-					{{ loop(comment.replies) }}
-				{% endif %}
-			</article>
-	{% endfor %}
-{% else %}
-	<p>There are no comments yet.<p>
-{% endif %}
-```
-## Recommendation
-Add a form, which allows your visitors to easily write comments.
-
-But more importantly, on submit the form generates a mailto-link.
-The resulting email contains a valid markdown block. Now you only have to copy this block in a new file. And therefore there is no need to gather the metadata (like date, author, replyto) yourself.
-
-##### Reply button
-Add this in the above `for` loop, so your visitors can reply to a comment.
-
-```html
-<button onclick="reply('{{comment.id | urlencode}}');">Reply</button>
-```
-
-##### form + javascript
-
-```html
-<form role="form" id="commentForm" action="#">
-	<input name="Name" type="text" id="commentForm_inputName" placeholder="Enter your name or synonym">
-	<textarea name="Text" id="commentForm_inputText" rows="10" style="resize:vertical;" placeholder="Your comment"></textarea>
-	<button type="submit" id="commentForm_button">Post via email</button>
-	<input name="replyto" type="hidden" id="commentForm_replyto">
-</form>
-```
-
-```javascript
-<script type="text/javascript">
-	function reply(id)
-	{
-		id = decodeURIComponent(id);
-		$('#commentForm_replyto').val(id);
-	}
-
-	$(document).ready(function() {
-		function generateMailToLink()
-		{
-			var user = 'your_user_name'; //user@domain = your email address
-			var domain = 'your_email_provider';
-			var subject = 'Comment for \'{{ article.slug }}\'' ;
-
-			var d = new Date();
-			var body = ''
-				+ 'Hey,\nI posted a new comment on ' + document.URL + '\n\nGreetings ' + $("#commentForm_inputName").val() + '\n\n\n'
-				+ 'Raw comment data:\n'
-				+ '----------------------------------------\n'
-				+ 'date: ' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + '\n'
-				+ 'author: ' + $("#commentForm_inputName").val() + '\n';
-
-			var replyto = $('#commentForm_replyto').val();
-			if (replyto.length != 0)
-			{
-				body += 'replyto: ' + replyto + '\n'
-			}
-
-			body += '\n'
-				+ $("#commentForm_inputText").val() + '\n'
-				+ '----------------------------------------\n';
-
-			var link = 'mailto:' + user + '@' + domain + '?subject='
-				+ encodeURIComponent(subject)
-				+ "&body="
-				+ encodeURIComponent(body);
-			return link;
-		}
-
-
-		$('#commentForm').on("submit",
-			function( event )
-			{
-				event.preventDefault();
-				$(location).attr('href', generateMailToLink());
-			}
-		);
-	});
-</script>
-```
-(jQuery is required for this script)
-
-Don't forget to set the Variables `user` and `domain`.
+## Instructions
+ - [Installation and basic usage](doc/installation.md)
+ - [Avatars and Identicons](doc/avatars.md)
+ - [Comment Form (aka: never gather Metadata)](doc/form.md)
+ 
+## Requirements
+To create identicons the Python Image Library is needed. Therefore you either need PIL **or** Pillow (recommended).
+
+##### Install Pillow
+	easy_install Pillow
+	
+If you don't use avatars or identicons this plugin works fine without PIL/Pillow. You will however get a warning that identicons are deactivated (as expected).

+ 92 - 0
pelican_comment_system/avatars.py

@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+"""
+
+"""
+
+import logging
+import os
+
+import hashlib
+
+
+logger = logging.getLogger(__name__)
+_log = "pelican_comment_system: avatars: "
+try:
+	from identicon import identicon
+	_identiconImported = True
+except ImportError as e:
+	logger.warning(_log + "identicon deactivated: " + str(e))
+	_identiconImported = False
+
+# Global Variables
+_identicon_save_path = None
+_identicon_output_path = None
+_identicon_data = None
+_identicon_size = None
+_initialized = False
+_authors = None
+_missingAvatars = []
+
+def _ready():
+	if not _initialized:
+		logger.warning(_log + "Module not initialized. use init")
+	if not _identicon_data:
+		logger.debug(_log + "No identicon data set")
+	return _identiconImported and _initialized and _identicon_data
+
+
+def init(pelican_output_path, identicon_output_path, identicon_data, identicon_size, authors):
+	global _identicon_save_path
+	global _identicon_output_path
+	global _identicon_data
+	global _identicon_size
+	global _initialized
+	global _authors
+	_identicon_save_path = os.path.join(pelican_output_path, identicon_output_path)
+	_identicon_output_path = identicon_output_path
+	_identicon_data = identicon_data
+	_identicon_size = identicon_size
+	_authors = authors
+	_initialized = True
+
+def _createIdenticonOutputFolder():
+	if not _ready():
+		return
+
+	if not os.path.exists(_identicon_save_path):
+		os.makedirs(_identicon_save_path)
+
+
+def getAvatarPath(comment_id, metadata):
+	if not _ready():
+		return ''
+
+	md5 = hashlib.md5()
+	author = tuple()
+	for data in _identicon_data:
+		if data in metadata:
+			string = str(metadata[data])
+			md5.update(string)
+			author += tuple([string])
+		else:
+			logger.warning(_log + data + " is missing in comment: " + comment_id)
+
+	if author in _authors:
+		return _authors[author]
+
+	global _missingAvatars
+
+	code = md5.hexdigest()
+
+	if not code in _missingAvatars:
+		_missingAvatars.append(code)
+
+	return os.path.join(_identicon_output_path, '%s.png' % code)
+
+def generateAndSaveMissingAvatars():
+	_createIdenticonOutputFolder()
+	for code in _missingAvatars:
+		avatar_path = '%s.png' % code
+		avatar = identicon.render_identicon(int(code, 16), _identicon_size)
+		avatar_save_path = os.path.join(_identicon_save_path, avatar_path)
+		avatar.save(avatar_save_path, 'PNG')

+ 35 - 0
pelican_comment_system/doc/avatars.md

@@ -0,0 +1,35 @@
+# Avatars and Identicons
+To activate the avatars and [identicons](https://en.wikipedia.org/wiki/Identicon) you have to set `PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`.
+
+##### Example
+```python
+PELICAN_COMMENT_SYSTEM_IDENTICON_DATA = ('author')
+```
+Now every comment with the same author tag will be treated as if written from the same person. And therefore have the same avatar/identicon. Of cause you can modify this tuple so other metadata are checked.
+
+## Specific Avatars
+To set a specific avatar for a author you have to add them to the `PELICAN_COMMENT_SYSTEM_AUTHORS` dictionary.
+
+The `key` of the dictionary has to be a tuple of the form of `PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`, so in our case only the author's name.
+
+The `value` of the dictionary is the path to the specific avatar.
+
+##### Example
+```python
+PELICAN_COMMENT_SYSTEM_AUTHORS = {
+	('John'): "images/authors/john.png",
+	('Tom'): "images/authors/tom.png",
+}
+```
+
+## Theme
+To display the avatars and identicons simply add the following in the "comment for loop" in you theme:
+
+```html
+<img src="{{ SITEURL }}/{{ comment.avatar }}"
+		alt="Avatar"
+		height="{{ PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE }}"
+		width="{{ PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE }}">
+```
+
+Of cause the `height` and `width` are optional, but they make sure that everything has the same size (in particular  specific avatars).

+ 83 - 0
pelican_comment_system/doc/form.md

@@ -0,0 +1,83 @@
+# Comment Form (aka: never gather Metadata)
+Add a form, which allows your visitors to easily write comments.
+
+But more importantly, on submit the form generates a mailto-link.
+The resulting email contains a valid markdown block. Now you only have to copy this block in a new file. And therefore there is no need to gather the metadata (like date, author, replyto) yourself.
+
+#### Reply button
+Add this in the "comment for loop" in your article theme, so your visitors can reply to a comment.
+
+```html
+<button onclick="reply('{{comment.id | urlencode}}');">Reply</button>
+```
+
+#### Form
+A basic form so your visitors can write comments.
+
+```html
+<form role="form" id="commentForm" action="#">
+	<input name="Name" type="text" id="commentForm_inputName" placeholder="Enter your name or synonym">
+	<textarea name="Text" id="commentForm_inputText" rows="10" style="resize:vertical;" placeholder="Your comment"></textarea>
+	<button type="submit" id="commentForm_button">Post via email</button>
+	<input name="replyto" type="hidden" id="commentForm_replyto">
+</form>
+```
+You may want to add a button to reset the `replyto` field.
+
+#### Javascript
+To generate the mailto-Link and set the `replyto` field there is some javascript required.
+
+```javascript
+<script type="text/javascript">
+	function reply(id)
+	{
+		id = decodeURIComponent(id);
+		$('#commentForm_replyto').val(id);
+	}
+
+	$(document).ready(function() {
+		function generateMailToLink()
+		{
+			var user = 'your_user_name'; //user@domain = your email address
+			var domain = 'your_email_provider';
+			var subject = 'Comment for \'{{ article.slug }}\'' ;
+
+			var d = new Date();
+			var body = ''
+				+ 'Hey,\nI posted a new comment on ' + document.URL + '\n\nGreetings ' + $("#commentForm_inputName").val() + '\n\n\n'
+				+ 'Raw comment data:\n'
+				+ '----------------------------------------\n'
+				+ 'date: ' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + '\n'
+				+ 'author: ' + $("#commentForm_inputName").val() + '\n';
+
+			var replyto = $('#commentForm_replyto').val();
+			if (replyto.length != 0)
+			{
+				body += 'replyto: ' + replyto + '\n'
+			}
+
+			body += '\n'
+				+ $("#commentForm_inputText").val() + '\n'
+				+ '----------------------------------------\n';
+
+			var link = 'mailto:' + user + '@' + domain + '?subject='
+				+ encodeURIComponent(subject)
+				+ "&body="
+				+ encodeURIComponent(body);
+			return link;
+		}
+
+
+		$('#commentForm').on("submit",
+			function( event )
+			{
+				event.preventDefault();
+				$(location).attr('href', generateMailToLink());
+			}
+		);
+	});
+</script>
+```
+(jQuery is required for this script)
+
+Don't forget to set the Variables `user` and `domain`.

+ 104 - 0
pelican_comment_system/doc/installation.md

@@ -0,0 +1,104 @@
+# Installation
+Activate the plugin by adding it to your `pelicanconf.py`
+
+	PLUGIN_PATH = '/path/to/pelican-plugins'
+	PLUGINS = ['pelican_comment_system']
+	PELICAN_COMMENT_SYSTEM = True
+
+And modify your `article.html` theme (see below).
+
+## Settings
+Name                                           | Type      | Default            | Description
+-----------------------------------------------|-----------|--------------------|-------
+`PELICAN_COMMENT_SYSTEM`                       | `boolean` | `False`            | Activates or deactivates the comment system
+`PELICAN_COMMENT_SYSTEM_DIR`                   | `string`  | `comments`         | Folder where the comments are stored
+`PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH` | `string`  | `images/identicon` | Output folder where the identicons are stored
+`PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`        | `tuple`   | `()`               | Contains all Metadata tags, which in combination identifies a comment author (like `('author', 'email')`)
+`PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE`        | `int`     | `72`               | Width and height of the identicons. Has to be a multiple of 3.
+`PELICAN_COMMENT_SYSTEM_AUTHORS`               | `dict`    | `{}`               | Comment authors, which should have a specific avatar. More infos [here](avatars.md)
+
+
+## Folder structure
+Every comment file has to be stored in a sub folder of `PELICAN_COMMENT_SYSTEM_DIR`.
+Sub folders are named after the `slug` of the articles.
+
+So the comments to your `foo-bar` article are stored in `comments/foo-bar/`
+
+The filenames of the comment files are up to you. But the filename is the Identifier of the comment (without extension).
+
+##### Example folder structure
+
+	.
+	└── comments
+		└── foo-bar
+		│   ├── 1.md
+		│   └── 0.md
+		└── some-other-slug
+			├── random-Name.md
+			├── 1.md
+			└── 0.md
+
+
+## Comment file
+### Meta information
+Tag           | Required  | Description
+--------------|-----------|----------------
+`date`        | yes       | Date when the comment was posted
+`replyto`     | no        | Identifier of the parent comment. Identifier = Filename (without extension)
+`locale_date` | forbidden | Will be overwritten with a locale representation of the date
+
+Every other (custom) tag gets parsed as well and will be available through the theme.
+
+##### Example of a comment file
+
+	date: 2014-3-21 15:02
+	author: Author of the comment
+	website: http://authors.website.com
+	replyto: 7
+	anothermetatag: some random tag
+
+	Content of the comment.
+
+## Theme
+In the `article.html` theme file are now two more variables available.
+
+Variables                         | Description
+----------------------------------|--------------------------
+`article.metadata.comments_count` | Amount of total comments for this article (including replies to comments)
+`article.metadata.comments`       | Array containing the top level comments for this article (no replies to comments)
+
+### Comment object
+Variables  | Description
+-----------|--------------------------
+`id`       | Identifier of this comment
+`content`  | Content of this comment
+`metadata` | All metadata as in the comment file (or described above)
+`replies`  | Array containing the top level replies for this comment
+`avatar`   | Path to the avatar or identicon of the comment author
+
+##### Example article.html theme
+(only the comment section)
+```html
+{% if article.metadata.comments %}
+	{% for comment in article.metadata.comments recursive %}
+		{% set metadata = comment.metadata %}
+		{% if loop.depth0 == 0 %}
+			{% set marginLeft = 0 %}
+		{% else %}
+			{% set marginLeft = 50 %}
+		{% endif %}
+			<article id="comment-{{comment.id}}" style="border: 1px solid #DDDDDD; padding: 5px 0px 0px 5px; margin: 0px -1px 5px {{marginLeft}}px;">
+				<a href="{{ SITEURL }}/{{ article.url }}#comment-{{comment.id}}" rel="bookmark" title="Permalink to this comment">Permalink</a>
+				<h4>{{ metadata['author'] }}</h4>
+				<p>Posted on <abbr class="published" title="{{ metadata['date'].isoformat() }}">{{ metadata['locale_date'] }}</abbr></p>
+
+				{{ comment.content }}
+				{% if comment.replies %}
+					{{ loop(comment.replies) }}
+				{% endif %}
+			</article>
+	{% endfor %}
+{% else %}
+	<p>There are no comments yet.<p>
+{% endif %}
+```

File diff suppressed because it is too large
+ 11 - 0
pelican_comment_system/identicon/LICENSE


+ 17 - 0
pelican_comment_system/identicon/README.md

@@ -0,0 +1,17 @@
+identicon.py: identicon python implementation.
+==============================================
+:Author:Shin Adachi <shn@glucose.jp>
+
+## usage
+
+### commandline
+
+    python identicon.py [code]
+
+### python
+
+    import identicon
+    identicon.render_identicon(code, size)
+
+Return a PIL Image class instance which have generated identicon image.
+`size` specifies patch size. Generated image size is 3 * `size`.

+ 0 - 0
pelican_comment_system/identicon/__init__.py


+ 255 - 0
pelican_comment_system/identicon/identicon.py

@@ -0,0 +1,255 @@
+#!/usr/bin/env python
+# -*- coding:utf-8 -*-
+"""
+identicon.py
+identicon python implementation.
+by Shin Adachi <shn@glucose.jp>
+
+= usage =
+
+== commandline ==
+>>> python identicon.py [code]
+
+== python ==
+>>> import identicon
+>>> identicon.render_identicon(code, size)
+
+Return a PIL Image class instance which have generated identicon image.
+```size``` specifies `patch size`. Generated image size is 3 * ```size```.
+"""
+# g
+# PIL Modules
+from PIL import Image, ImageDraw, ImagePath, ImageColor
+
+
+__all__ = ['render_identicon', 'IdenticonRendererBase']
+
+
+class Matrix2D(list):
+    """Matrix for Patch rotation"""
+    def __init__(self, initial=[0.] * 9):
+        assert isinstance(initial, list) and len(initial) == 9
+        list.__init__(self, initial)
+
+    def clear(self):
+        for i in xrange(9):
+            self[i] = 0.
+
+    def set_identity(self):
+        self.clear()
+        for i in xrange(3):
+            self[i] = 1.
+
+    def __str__(self):
+        return '[%s]' % ', '.join('%3.2f' % v for v in self)
+
+    def __mul__(self, other):
+        r = []
+        if isinstance(other, Matrix2D):
+            for y in xrange(3):
+                for x in xrange(3):
+                    v = 0.0
+                    for i in xrange(3):
+                        v += (self[i * 3 + x] * other[y * 3 + i])
+                    r.append(v)
+        else:
+            raise NotImplementedError
+        return Matrix2D(r)
+
+    def for_PIL(self):
+        return self[0:6]
+
+    @classmethod
+    def translate(kls, x, y):
+        return kls([1.0, 0.0, float(x),
+                    0.0, 1.0, float(y),
+                    0.0, 0.0, 1.0])
+
+    @classmethod
+    def scale(kls, x, y):
+        return kls([float(x), 0.0, 0.0,
+                    0.0, float(y), 0.0,
+                    0.0, 0.0, 1.0])
+
+    """
+    # need `import math`
+    @classmethod
+    def rotate(kls, theta, pivot=None):
+        c = math.cos(theta)
+        s = math.sin(theta)
+
+        matR = kls([c, -s, 0., s, c, 0., 0., 0., 1.])
+        if not pivot:
+            return matR
+        return kls.translate(-pivot[0], -pivot[1]) * matR *
+            kls.translate(*pivot)
+    """
+    
+    @classmethod
+    def rotateSquare(kls, theta, pivot=None):
+        theta = theta % 4
+        c = [1., 0., -1., 0.][theta]
+        s = [0., 1., 0., -1.][theta]
+
+        matR = kls([c, -s, 0., s, c, 0., 0., 0., 1.])
+        if not pivot:
+            return matR
+        return kls.translate(-pivot[0], -pivot[1]) * matR * \
+            kls.translate(*pivot)
+
+
+class IdenticonRendererBase(object):
+    PATH_SET = []
+    
+    def __init__(self, code):
+        """
+        @param code code for icon
+        """
+        if not isinstance(code, int):
+            code = int(code)
+        self.code = code
+    
+    def render(self, size):
+        """
+        render identicon to PIL.Image
+        
+        @param size identicon patchsize. (image size is 3 * [size])
+        @return PIL.Image
+        """
+        
+        # decode the code
+        middle, corner, side, foreColor, backColor = self.decode(self.code)
+
+        # make image        
+        image = Image.new("RGB", (size * 3, size * 3))
+        draw = ImageDraw.Draw(image)
+        
+        # fill background
+        draw.rectangle((0, 0, image.size[0], image.size[1]), fill=0)
+        
+        kwds = {
+            'draw': draw,
+            'size': size,
+            'foreColor': foreColor,
+            'backColor': backColor}
+        # middle patch
+        self.drawPatch((1, 1), middle[2], middle[1], middle[0], **kwds)
+
+        # side patch
+        kwds['type'] = side[0]
+        for i in xrange(4):
+            pos = [(1, 0), (2, 1), (1, 2), (0, 1)][i]
+            self.drawPatch(pos, side[2] + 1 + i, side[1], **kwds)
+        
+        # corner patch
+        kwds['type'] = corner[0]
+        for i in xrange(4):
+            pos = [(0, 0), (2, 0), (2, 2), (0, 2)][i]
+            self.drawPatch(pos, corner[2] + 1 + i, corner[1], **kwds)
+        
+        return image
+                
+    def drawPatch(self, pos, turn, invert, type, draw, size, foreColor,
+            backColor):
+        """
+        @param size patch size
+        """
+        path = self.PATH_SET[type]
+        if not path:
+            # blank patch
+            invert = not invert
+            path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)]
+        patch = ImagePath.Path(path)
+        if invert:
+            foreColor, backColor = backColor, foreColor
+        
+        mat = Matrix2D.rotateSquare(turn, pivot=(0.5, 0.5)) *\
+              Matrix2D.translate(*pos) *\
+              Matrix2D.scale(size, size)
+        
+        patch.transform(mat.for_PIL())
+        draw.rectangle((pos[0] * size, pos[1] * size, (pos[0] + 1) * size,
+            (pos[1] + 1) * size), fill=backColor)
+        draw.polygon(patch, fill=foreColor, outline=foreColor)
+
+    ### virtual functions
+    def decode(self, code):
+        raise NotImplementedError
+
+
+class DonRenderer(IdenticonRendererBase):
+    """
+    Don Park's implementation of identicon
+    see : http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released
+    """
+    
+    PATH_SET = [
+        [(0, 0), (4, 0), (4, 4), (0, 4)],   # 0
+        [(0, 0), (4, 0), (0, 4)],
+        [(2, 0), (4, 4), (0, 4)],
+        [(0, 0), (2, 0), (2, 4), (0, 4)],
+        [(2, 0), (4, 2), (2, 4), (0, 2)],   # 4
+        [(0, 0), (4, 2), (4, 4), (2, 4)],
+        [(2, 0), (4, 4), (2, 4), (3, 2), (1, 2), (2, 4), (0, 4)],
+        [(0, 0), (4, 2), (2, 4)],
+        [(1, 1), (3, 1), (3, 3), (1, 3)],   # 8   
+        [(2, 0), (4, 0), (0, 4), (0, 2), (2, 2)],
+        [(0, 0), (2, 0), (2, 2), (0, 2)],
+        [(0, 2), (4, 2), (2, 4)],
+        [(2, 2), (4, 4), (0, 4)],
+        [(2, 0), (2, 2), (0, 2)],
+        [(0, 0), (2, 0), (0, 2)],
+        []]                                 # 15
+    MIDDLE_PATCH_SET = [0, 4, 8, 15]
+    
+    # modify path set
+    for idx in xrange(len(PATH_SET)):
+        if PATH_SET[idx]:
+            p = map(lambda vec: (vec[0] / 4.0, vec[1] / 4.0), PATH_SET[idx])
+            PATH_SET[idx] = p + p[:1]
+    
+    def decode(self, code):
+        # decode the code        
+        middleType  = self.MIDDLE_PATCH_SET[code & 0x03]
+        middleInvert= (code >> 2) & 0x01
+        cornerType  = (code >> 3) & 0x0F
+        cornerInvert= (code >> 7) & 0x01
+        cornerTurn  = (code >> 8) & 0x03
+        sideType    = (code >> 10) & 0x0F
+        sideInvert  = (code >> 14) & 0x01
+        sideTurn    = (code >> 15) & 0x03
+        blue        = (code >> 16) & 0x1F
+        green       = (code >> 21) & 0x1F
+        red         = (code >> 27) & 0x1F
+        
+        foreColor = (red << 3, green << 3, blue << 3)
+        
+        return (middleType, middleInvert, 0),\
+               (cornerType, cornerInvert, cornerTurn),\
+               (sideType, sideInvert, sideTurn),\
+               foreColor, ImageColor.getrgb('white')
+
+
+def render_identicon(code, size, renderer=None):
+    if not renderer:
+        renderer = DonRenderer
+    return renderer(code).render(size)
+
+
+if __name__ == '__main__':
+    import sys
+    
+    if len(sys.argv) < 2:
+        print 'usage: python identicon.py [CODE]....'
+        raise SystemExit
+    
+    for code in sys.argv[1:]:
+        if code.startswith('0x') or code.startswith('0X'):
+            code = int(code[2:], 16)
+        elif code.startswith('0'):
+            code = int(code[1:], 8)
+        else:
+            code = int(code)
+        
+        icon = render_identicon(code, 24)
+        icon.save('%08x.png' % code, 'PNG')

+ 37 - 8
pelican_comment_system/pelican_comment_system.py

@@ -18,12 +18,15 @@ from pelican import signals
 from pelican.utils import strftime
 from pelican.readers import MarkdownReader
 
+import avatars
+
 class Comment:
-	def __init__(self, id, metadata, content):
+	def __init__(self, id, metadata, content, avatar):
 		self.id = id
 		self.content = content
 		self.metadata = metadata
 		self.replies = []
+		self.avatar = avatar
 
 	def addReply(self, comment):
 		self.replies.append(comment)
@@ -53,14 +56,31 @@ class Comment:
 		return amount + len(self.replies)
 
 
-def initialized(pelican):
+def pelican_initialized(pelican):
 	from pelican.settings import DEFAULT_CONFIG
 	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM', False)
 	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_DIR' 'comments')
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH' 'images/identicon')
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
+	DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
 	if pelican:
 		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM', False)
 		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_DIR', 'comments')
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH', 'images/identicon')
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
+		pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
+
 
+def initialize(article_generator):
+	avatars.init(
+		article_generator.settings['OUTPUT_PATH'],
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH'],
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_DATA'],
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE']/3,
+		article_generator.settings['PELICAN_COMMENT_SYSTEM_AUTHORS'],
+		)
 
 def add_static_comments(gen, metadata):
 	if gen.settings['PELICAN_COMMENT_SYSTEM'] != True:
@@ -73,21 +93,25 @@ def add_static_comments(gen, metadata):
 		logger.warning("pelican_comment_system: cant't locate comments files without slug tag in the article")
 		return
 
-	reader = MarkdownReader(gen.settings)
-	comments = []
-	replies = []
 	folder = os.path.join(gen.settings['PELICAN_COMMENT_SYSTEM_DIR'], metadata['slug'])
 
 	if not os.path.isdir(folder):
 		logger.debug("No comments found for: " + metadata['slug'])
 		return
 
+	reader = MarkdownReader(gen.settings)
+	comments = []
+	replies = []
+
 	for file in os.listdir(folder):
 		name, extension = os.path.splitext(file)
 		if extension[1:].lower() in reader.file_extensions:
-			content, meta = reader.read(folder + "/" + file)
+			content, meta = reader.read(os.path.join(folder, file))
 			meta['locale_date'] = strftime(meta['date'], gen.settings['DEFAULT_DATE_FORMAT'])
-			com = Comment(name, meta, content)
+
+			avatar_path = avatars.getAvatarPath(name, meta)
+			com = Comment(name, meta, content, avatar_path)
+
 			if 'replyto' in meta:
 				replies.append( com )
 			else:
@@ -109,6 +133,11 @@ def add_static_comments(gen, metadata):
 	metadata['comments_count'] = len(comments) + count
 	metadata['comments'] = comments
 
+def writeIdenticonsToDisk(gen, writer):
+	avatars.generateAndSaveMissingAvatars()
+
 def register():
-	signals.initialized.connect(initialized)
+	signals.initialized.connect(pelican_initialized)
+	signals.article_generator_init.connect(initialize)
 	signals.article_generator_context.connect(add_static_comments)
+	signals.article_writer_finalized.connect(writeIdenticonsToDisk)