skeletonz - The simple Python CMS system

The goal of this tutorial is to explain Skeletonz plugin creation basics through the creation of a real plugin: the ImgTitle plugin, that is part of Skeletonz.

The MyImgTitle plugin aims at transforming the text that is provided as a parameter in an image generated from a predefined font.

To run this example and use this plugin, you'll need:

Syntax:

[imgtitle=this is my new title]

When rendered:

render example

What is a plugin used for?

There's two ways of using plugins:

  • first way is to generate new wiki instructions
  • second way is to add new pages that can be served directly by Skeletonz engine (i.e. see GreyBox image)

Step 1: Base plugin creation

Creating a plugin is dead simple:

1. Create a new folder named myimgtitle under folder site_plugins

2. Create a __init__.py file under that folder (plugins are in fact python modules. This file is here to describe the new module we're creating.)
Here is the content of the file (dead simple :) )

#

3. Create a plugin.py with the following content:

from skeletonz.modules.plugin import GenericPlugin
from skeletonz.server import getFormatManager

PLUGINS_FOR_EXPORT = ['MyImgTitle']

class MyImgTitle(GenericPlugin):

    def __init__(self):
        getFormatManager().registerSLPlugin('imgtitle', self.generateImage)

    def generateImage(self, args, edit_mode, page):
        return False, "<b>This is my brand new plugin output !</b>"

Let's explain this a little bit:

  • GenericPlugin is the base class of all plugins. All the plugins you will create will inherit it.
  • PLUGINS_FOR_EXPORT is a list of all class name in the plugin that should be availble (technically speaking, it is mainly used for the plugin management section of the admin panel to automatically detect and use new plugins).
  • MyImgTitle(GenericPlugin) is our new plugin main class.
  • getFormatManager().registerSLPlugin helps to link a wiki keyword to a plugin behaviour. As you can see for instance in the PersonalTable plugin, several keywords can be defined for a plugin, each with its own behaviour. in this case, each time a "myimgtitle" will be found in the page, its content will be replaced by the output of generateImage.
  • generateImage's output will replace our keyword in the page. It should return a tuple (is_block_elm, output), where is_block_elm tells the wiki engine if content is a block (i.e. p, div, h1 etc).

An example will make it bright clear: just create a new page and put our brand new keyword in it

[myimgtitle]

Save your changes... On the saved page you should see a bold "This is my brand new plugin output !" in place of your keyword

Step 2: Using arguments

It would not be very funny if this plugin only use was to put the same text again and again... So we'll have to add a bit of variables to our plugin, in our case:

  • the text that will be rendered
  • the font that will be used
  • the size we'll apply

The syntax for the plugin will be the following one:

[myimgtitle=my own title, font=FreeSans.ttf, size=32]

In order to achieve this goal, the only thing we will modify is the generateImage function.
In the parameters this function accepts, we will focus on args which is the dictionnary of arguments provided to the plugin.

So, we can modify the function to handle arguments

def generateImage(self,args,edit_mode,page_id):
    img_folder = 'dynamic_dirs/generated/'
    font_path = '/usr/share/fonts/truetype/freefont'
    default_font = 'Courier.ttf'
    default_size = '48'
    title = args.get('imgtitle', "No title provided !")
    
    # Check defaults and args and load the requested font if the image does not already exists
    font = args.get('font', default_font)
    size = int(args.get('size', default_size))
    font_file = os.path.join(font_path,font)
    
    img_file = self.toFileName(title,font,size)
    if not os.path.isfile(os.path.join(img_folder,img_file)):
      imgfont = ImageFont.truetype(font_file, size)
      # Dummy image creation to size the image, then real image generation
      img = Image.new("RGBA",(500,500),(255,255,255))
      draw = ImageDraw.Draw(img)
      width,height = draw.textsize(title, font=imgfont)
      img = img.resize((width,height))
      draw = ImageDraw.Draw(img)
      draw.text((0,0), title,fill=(0,0,0),font=imgfont)
      img.save(os.path.join(img_folder,img_file))
  
    return False, "<img src='/generated/%s'/>" % img_file

Several things can be noticed in this code:

  • We never assume the arguments will be provided: it is better to allow a default behaviour (see how args.get('parameter', default_value) are used instead of args['parameter'])
  • images are generated only once in the folder dynamic_dirs/generated folder

Step 3: Adding plugin options

Before plublishing your plugin, you can ease it use for the people who wants to install it by adding plugin options.
Plugin options are plugin parameters you can set directly from Skeletonz admin panel, plugin section. Here is a screen copy of how our ImgTitle options will look:

imgtitle options

Options just have to be declared in the plugin class itself (look for PLUGIN_OPTIONS in the example below) and you'll got automatic option save and restore (i.e. the paramters are automatically reloaded when restart your Skeletonz server

The code should speak for itself, so here's our plugin updated:

import os
from PIL import Image,ImageDraw,ImageFont

from skeletonz.modules.plugin import GenericPlugin
from skeletonz.user_plugins import TextOption
from skeletonz.server import getFormatManager

PLUGINS_FOR_EXPORT = ['ImgTitle']

class ImgTitle(GenericPlugin):
    NAME = "Image Title plugin"
    DESCRIPTION = "Generate images from fonts and text.<br />Python Image Library (PIL) required."
    SYNTAX = [
        {'handler': 'imgtitle',
         'required_arguments': {'ident':'Text to be converted'},
         'optional_arguments': {'font':'Font to be used', 'size':'Size to be used'}
        },
      ]
    PLUGIN_OPTIONS = [
            TextOption(scope='ImgTitle',name="font_path",text="Font path",default="/usr/share/fonts/truetype/freefont"),
            TextOption(scope='ImgTitle',name="default_font",text="Default font",default="FreeSans.ttf"),
            TextOption(scope='ImgTitle',name="default_size",text="Default size",default="16")
                      ]
    def __init__(self):
        getFormatManager().registerSLPlugin('imgtitle', self.generateImage)

    def toFileName(self,text,font,size):
        filename = "%s_%s_%s.png" % (text, font, str(size))
        return filename.replace(' ','_')

    def generateImage(self,args,edit_mode,page_id):
        title = args.get('imgtitle', "No title provided !")

        # Check defaults and args and load the requested font if the image does not already exists
        font = args.get('font', self.optionValue('default_font'))
        size = int(args.get('size', self.optionValue('default_size')))
        font_file = os.path.join(self.optionValue('font_path'),font)

        img_file = self.toFileName(title,font,size)
        if not os.path.isfile(os.path.join(self.img_folder,img_file)):
            imgfont = ImageFont.truetype(font_file, size)
            # Dummy image creation to size the image, then real image generation
            img = Image.new("RGBA",(500,500),(255,255,255))
            draw = ImageDraw.Draw(img)
            width,height = draw.textsize(title, font=imgfont)
            img = img.resize((width,height))
            draw = ImageDraw.Draw(img)
            draw.text((0,0), title,fill=(0,0,0),font=imgfont)
            img.save(os.path.join(self.img_folder,img_file))

        return False, "<img src='/generated/imgtitle/%s'/>" % img_file

    def createStructure(self):
        self.img_folder = 'dynamic_dirs/generated/imgtitle'
        if not os.path.isdir(self.img_folder):
            os.mkdir(self.img_folder)
        return []

You can see that we added some other things when implementing plugins:

  • PLUGIN_OPTIONS is a list of required options.
    Options are stored in a text file under trunk/dyanmic_files/plugins.
    scope is defining how options will be separated in the file (i.e. if several plugins share the same scope, they can have common options)
  • we modified generateImage to handle our options.
  • we added a createStructure function which role is to create the folder in which rendered images will be stored. You can notice that if the plugin detects the image already exists, it does not try to recreate it, but use the alreadt-rendered image instead (less CPU needed).

Step 4: Adding plugin auto-documentation (feature planned for Skeletonz 1.1)

to be continued ...

Powered by Skeletonz