サムネイル自動生成追加

ごく普通のアップローダー(まだまだ未完成)を
Pythonの勉強がてら作ってみています。

本日やってみたかった自動サムネイル生成機能を追加しました。

使用したライブラリー等のリンクを以下に。

O/R Mapper (DataMapper)
SQLAlchemy - The Database Toolkit for Python
http://www.sqlalchemy.org/

画像操作
Python Imaging Library (PIL)
http://www.pythonware.com/products/pil/

ウェブフレームワーク
CherryPy - a pythonic, object-oriented web development framework
http://www.cherrypy.org/

XML操作?(cElementTreeを使用)
ElementTree Overview ::: www.effbot.org
http://effbot.org/zone/element-index.htm

HTMLテンプレート
Kid
http://kid.lesscode.org/index.html

Ajaxな画像表示
Lightbox JS v2.0
http://www.huddletogether.com/projects/lightbox2/


以下、スクリーンショットを数点。

何もアップロードしていない状態
f:id:Voluntas:20060713223600p:image

いくつかの画像をアップロードした状態
f:id:Voluntas:20060713223611p:image

サムネイル画像をクリックして大きい画像を表示した状態。
f:id:Voluntas:20060713223614p:image

なぜかお気に入りのURL
f:id:Voluntas:20060713224009p:image


CherryPyで使用しているconfファイルですが、
lightboxを使うためのstaticなフォルダ設定に使っています。

upload.py


#Python 2.4.3
import base64
#Kid 0.9.2
import kid
#PIL 1.1.5
import Image
#CherryPy 2.2.1
import cherrypy
from cherrypy import expose
#SQLAlchemy 0.2.5
from sqlalchemy import *
#cElementTree 1.0.5
from cElementTree import *

#lightbox 2.02

from model import *

class Root(object):
    
    def __init__(self):
        
        mapper(Thumbnail, thumbnails_table, properties ={
            'data': deferred(thumbnails_table.c.data)
        })
        mapper(File, files_table, properties = {
            'data': deferred(files_table.c.data),
            'thumbnails': relation(Thumbnail,
                                    cascade="all, delete-orphan")
        })
        
        self.session = create_session()
    
    @expose
    def index(self):
        
        template = kid.Template(file='index.kid', foo=self.viewImage())

        return template.serialize()
    
    @expose
    def upload(self, uploadFile):
        
        if self.checkFile(uploadFile.file):
            
            temp = File(
                name=uploadFile.filename,
                type=uploadFile.type,
                data=uploadFile.value
            )
            
            temp.thumbnails.append(Thumbnail(
                uploadFile.filename,
                uploadFile.file))
            
            self.session.save(temp)
            self.session.flush()
    
            template = kid.Template(file='index.kid', foo=self.viewImage())
    
            return template.serialize()

        else:
            return "this file was not image file."
    
    @expose
    def image(self, address):
        L = self.session.query(File).select_by(
            files_table.c.address==address)
        return base64.b64decode(L[0].data)
    
    @expose
    def thumb(self, address):
        L = self.session.query(Thumbnail).select_by(
            thumbnails_table.c.address==address)
        return base64.b64decode(L[0].data)

    def checkFile(self, file):
        try:
            im = Image.open(file)
            if im.format in ["JPEG", "PNG"]:
                return True
            else:
                raise IOError
        except IOError:
            return False
    
    def viewImage(self):
        L = self.session.query(File).select()
        SPAN = Element('span')
        
        if len(L) != 0:
            for i in L:
                A = Element('a', {'title': i.name,
                                  'rel': 'lightbox[group]',
                                  'href': 'image/' + i.address
                                  })
                IMG = Element('img', {'src': 'thumb/' + i.thumbnails[0].address,
                                      'alt': i.thumbnails[0].name,
                                      'border': '0'})
                A.append(IMG)
                SPAN.append(A)
    
            return SPAN
        else:
            return "Not image in DB."

cherrypy.root = Root()

if __name__=='__main__':

    cherrypy.config.update(file='test.conf')
    cherrypy.server.start()

データベース関連
まだまだ下手すぎ

model.py


import base64
import Image
import string
import random
from sqlalchemy import *

sqlite_db = create_engine('sqlite:///1.db')
metadata = BoundMetaData(sqlite_db)

files_table = \
    Table('files', metadata,
        Column('file_id', Integer, primary_key = True),
        Column('name', String),
        Column('comment', String),
        Column('type', String),
        Column('address', String, unique=True),
        Column('data', Binary),
    )
thumbnails_table = \
    Table('thumbnails', metadata,
        Column('thumb_id', Integer, primary_key = True),
        Column('address', String, unique=True),
        Column('name', String),
        Column('data', Binary),
        Column('file_id', Integer, ForeignKey("files")),
    )
metadata.create_all()

class Abstract(object):
    def getRandomAscii(self, length):
        self.L = list(string.ascii_letters + '0123456789')
        
        key = ""
        for i in range(length):
            key += random.choice(self.L)
        return key

class File(Abstract):
    def __init__(self, name, type, data, comment=""):
        
        self.file_id = None
        self.name = name
        self.comment = comment
        self.type = type
        self.address = self.getRandomAscii(16)
        self.data = base64.b64encode(data)

class Thumbnail(Abstract):
    def __init__(self, name, data):

        self.thumb_id = None
        self.name = "thumb_" + name
        self.address = self.getRandomAscii(16)
        self.data = self._createThumb(data)

    def _createThumb(self, file):
        im = Image.open(file, 'r')
        im.thumbnail*1
        return base64.b64encode(im2.tostring('jpeg', 'RGB'))

こんな感じです。

それにしても無駄に長い…、
まだまだ改良点があるなぁと実感しました。

一番引っかかったのはPILで画像オブジェクトを文字列に変更する際

im.tostring("jpeg", "RGB")

のRGBの部分ですorz

CherryPyとPILはもう少しわかりやすいドキュメントをー!!
ソース読めって?(がむばります

次の課題
・サムネイルをランダム生成
(いまは画像の一部をクロップして生成してます)
MochiKitを使用してAjaxな感じを
・もっとリファクタリング
・uselist=Falseを使いつつもマッピングをきれいに。

今回学習したこと
・KidはElementTreeのオブジェクトを直接受け取れる。
・PILのtostoringの引数はファイルタイプとRGBとかなんかよくわからないの。
CherryPyがフォームから受け取るファイルのファイルタイプはcgiのFieldStorageタイプ。

*1:256, 256), Image.ANTIALIAS)         im2 = im.crop((0, 0, 150, 50