Pic2Plot plugin

This plugin will convert the PIC Graphical Language to an image. See http://en.wikipedia.org/wiki/Pic_language for more info on the PIC language. You can use this to create all sorts of freeform drawings and diagrams.

For examples see http://www.onlamp.com/pub/a/onlamp/2007/06/21/in-praise-of-pic.html?page=1

A good description can be found here: http://floppsie.comp.glam.ac.uk/Glamorgan/gaius/web/pic.html

Another nice resource is http://www.kohala.com/start/troff/troff.html specifically http://www.kohala.com/start/troff/pic.examples.ps and the PIC manual at http://www.kohala.com/start/troff/cstr116.ps. You will need GSView to open the .ps files.

You can also use this to write UML sequence diagrams if you get the sequence.pic file from the UMLGraph package at http://www.umlgraph.org/. This is described here.

You can also use this plugin to create custom shapes to be used with Graphviz. Just right click the generated image and save it, then include it as a nodeshape in your Graphviz diagram.

Just for fun I wrote a PIC script to generate SuperFormula shapes. See Examples further down the page.

Prerequisites

You will need pic2plot which is included in GNU PlotUtils

The output image has a large canvas. If you want to crop this you will also need ImageMagick

The plugin has an options panel where you can configure the following settings:

  • path to the pic2plot executable (mandatory)
  • path to ImageMagick folder
  • always crop output image

Examples

The images are drawn by the code below it:

Stick figure

stickfigure

[:pic:'''
.PS
down; circle; line .1;
{line right .65}
{line left .65}
line
{line down .65 left .4;}
line down .65 right .4
.PE
''';crop]

Simple Shapes

pic example

[:pic:'''
.PS
box "box";
move;
line "line" "";
move;
arrow "arrow" "";
move;
circle "circle";
move;
ellipse "ellipse";
move;
arc; down; move; "arc"
.PE
''';crop]

Freeform diagram

Simple diagram

[:pic:'''
.PS 6
box "START"; arrow; circle dashed filled; arrow
circle diam 2 thickness 3 "This is a" "big, thick" "circle" dashed; up
arrow from top of last circle; ellipse "loopback" dashed
arrow dotted from left of last ellipse to top of last box
arc cw radius 1/2 from top of last ellipse; arrow
box "END"
.PE
''';'args=-F HersheySans -f .02';'crop']

Arguments

The plugin supports passing arguments to pic2plot. The arguments appendix has to start with 'args=' and must be completely quoted. The following example will make the image larger, and use a specified font:

[:pic:'''
.PS
arrow "ow!"
.PE
''';'args=-F HersheySans --bitmap-size 1000x1000']

Crop

If you want to crop the whitespace around the picture you can do it like this:

[:pic:'''
.PS
arrow "ow!"
.PE
''';crop]

You can combine this with arguments:

[:pic:'''
.PS
arrow "ow!"
.PE
''';'args=-F HersheySans --bitmap-size 1000x1000';crop]

Super Formula PIC script

superformula

The script below contains a macro for the SuperFormula and is used to create the image above:

[:pic:///
.PS 7
# Custom formula shapes 
#
# sf2d(a, b, m, n1, n2, n3, [periods, [transformation, [rotation]]])
#
# the following transformations are possible
#    0: None
#    1: x
#    2: sin(x)
#    3: cos(x)
#    4: sqrt(x)
#    5: sin(x)²
#    6: cos(x)²
#    7: 1+sin(x)²
#    8: 1+cos(x)²
#    9: atan(x)
# 
define sf2d {
    pi = atan2(0,-1)
    a = $1
    b = $2
    
    m = $3
    n1 = $4
    n2 = $5
    n3 = $6
    start = 0

    if ("$7" == "") then {
        stop = 2 * pi
        periods = 1024
    } else {
        periods = $7
        stop = 2048/periods * pi
    }
    step = pi / periods

    if ("$8" == "") then {transformation = 0} else {transformation = $8}
    if ("$9" == "") then {degrees = 0} else {degrees = $9}

    S: [
    
        for phi = start to stop by step do {

            if transformation == 1 then { f = phi } else {
            if transformation == 2 then { f = sin(phi) } else {
            if transformation == 3 then { f = cos(phi) } else {
            if transformation == 4 then { f = sqrt(phi) } else {
            if transformation == 5 then { f = sin(phi)^2 } else {
            if transformation == 6 then { f = cos(phi)^2 } else {
            if transformation == 7 then { f = 1+sin(phi)^2 } else {
            if transformation == 8 then { f = 1+cos(phi)^2 } else {
            if transformation == 9 then { f = atan2(phi, 1) } else {
                f = 1
            }}}}}}}}}

            r1 = cos(phi*m/4)/a
            if (r1 < 0) then {r1 = -r1};
            r2 = sin(phi*m/4)/b
            if (r2 < 0) then {r2 = -r2};
           
            r = (r1^n2 + r2^n3)^(1/n1) 

            if (r == 0) then {
                x = 0
                y = 0
            } else {    
                r = f/r
                x = r * cos(phi) 
                y = r * sin(phi)
            }

            # rotation
            if (degrees != 0) then {
                angle = degrees * pi/180
                xOld = x
                yOld = y
                x = (xOld * cos( angle )) - (yOld * sin( angle ))
                y = (xOld * sin( angle )) + (yOld * cos( angle ))
            }
            if (phi == start) then {
                move to (x, -y)
            }
            line to (x, -y)
        }
    ]
}

sf2d(1, 1, 12, -2, 1, 1); {"generating" at last [].c}; move;
sf2d(3, 2, 4, 30, 15, 15); {"Superformula" at last [].c}; move;
sf2d(1, 1, 4, 3, 1, 1); {"shapes" at last [].c}; move;
sf2d(1, 1, 8, 1, 1, 8); {"is" at last [].c}; move;
sf2d(1, 1, 8, 12, -3, 8); {"FUN" at last [].c};
.PE
///;"args=-F HersheySans -f .02 --bitmap-size 1000x1000";crop]

Some other examples:

Superformula sf2d(1, 1, 5.3, 0.4, 0, 6, 0, 64); Superformula sf2d(1, 1, 50, 6, 15, 15);Superformula sf2d(1, 0.6, 30, 75, 1.5, 35); Superformula sf2d(1, 1, 5, 3, 6, 6, 128, 1, 18);

UML sequence diagram

Sequence diagram

[:pic:'''
.PS
copy "\full\path\to\sequence.pic";

# Define the objects
object(O,"o:Toolkit");
placeholder_object(P);
step();

# Activation and messages
active(O);
message(O,O,"callbackLoop()");
create_message(O,P,"p:Peer");
message(O,P,"handleExpose()");
active(P);
return_message(P,O,"");
inactive(P);
destroy_message(O,P);
inactive(O);

# Complete the lifeline of O
step();
complete(O);
.PE
''';'args=-F HersheySans --bitmap-size 1000x1000';crop]

A more complex example from the UMLGraph manual:

More complex sequence diagram

[:pic:'''
# usage-example for the comment extensions to the
# Written by Sebastian Setzer

.PS
copy "\full\path\to\sequence.pic";

# Define the objects
actor(U, "");
object(G,"g:GUI");
placeholder_object(Dummy1); # more space
placeholder_object(D);
step();

# Message sequences
active(G);
step();

create_message(G,D,"db:DB");
active(D);
step();
inactive(D);

async(); # use asynchrone messages (not-filled arrowheads)

comment(D,C,down 1 right, wid 1 ht 0.7 "waiting for" "condition" "dbMailbox")

message(U,G,"openBrowser");
message(G,D,"query()"); active(D);
message(D,G,"result"); inactive(D);

connect_to_comment(D,C)

message(U,G,"scroll");
message(G,D,"query()"); active(D);
message(D,G,"result"); inactive(D);

connect_to_comment(D,C)

message(U,G,"Exit");
step();
begin_frame(G,F,"OnExit");
message(G,D,"shutdown()"); inactive(G); active(D);
comment(G,C,down .2 right .2, wid 2 ht 0.25 "wait for cond. dbShutdown")
step();
comment(D,C,right, wid 2 ht 1 \
        "all queries preceeding the" \
        "shutdown in the mailbox" \
        "are answered already." \
        "DbQuery-Objects can" \
        "be destroyed")
message(D,G,"done"); inactive(D); active(G);

sync();
destroy_message(G,D,"");

step();
end_frame(D,F);

step();
inactive(G);

# Complete the lifelines
step();
complete(G);
complete(U);
.PE

''';'args=-F HersheySans --bitmap-size 1000x1000';'crop']

Have fun!

Sourcecode

Save the code below to a file called pic2plot.py in your user_extensions folder in the wikidpad program folder.

"""
2009-05-20: created by bousch_AT_gmail.com
Do with it what you want, no warranty whatsoever.

Changes:
    2009-06-20: added option to always crop output images
    2009-06-20: fixed error when exporting to directory with spaces in name
    2009-05-25: changed argument pattern

Description:

This plugin enables you to use PIC Graphic language fragments in your pages
which are rendered as PNG images when previewed or exported.

Save this file to your WikidPad/extensions folder

Dependencies:
    GNU Plotutis
    ImageMagick (only if you want the images cropped automatically)

Arguments:

    pic2plot:
        If you want to pass arguments to pic2plot use the
        'args=' string as an appendix, see example
    
    crop:
        pic2plot renders an image on a large canvas.
        Use the crop argument if you want the image cropped.
        You can also use the 'Always crop image' option.

Example:

    [:pic:'''
    .PS 6
    box "START"; arrow; circle dashed filled; arrow
    circle diam 2 thickness 3 "This is a" "big, thick" "circle" dashed; up
    arrow from top of last circle; ellipse "loopback" dashed
    arrow dotted from left of last ellipse to top of last box
    arc cw radius 1/2 from top of last ellipse; arrow
    box "END"
    .PE
    ''';'args=-F HersheySans -f .02';'crop']

"""
import os, os.path
from subprocess import list2cmdline
import time

import wx

from pwiki.TempFileSet import createTempFile
from pwiki.StringOps import mbcsEnc, mbcsDec, lineendToOs

WIKIDPAD_PLUGIN = (("InsertionByKey", 1), ("Options", 1))
LOG = False

def describeInsertionKeys(ver, app):
    """
    API function for "InsertionByKey" plugins
    Returns a sequence of tuples describing the supported
    insertion keys. Each tuple has the form (insKey, exportTypes, handlerFactory)
    where insKey is the insertion key handled, exportTypes is a sequence of
    strings describing the supported export types and handlerFactory is
    a factory function (normally a class) taking the wxApp object as
    parameter and returning a handler object fulfilling the protocol
    for "insertion by key" (see EqnHandler as example).

    ver -- API version (can only be 1 currently)
    app -- wxApp object
    """
    return (
            (u"pic", ("html_single", "html_previewWX", "html_preview", "html_multi"), P2PHandler),
            )


class P2PHandler:
    """
    Base class fulfilling the "insertion by key" protocol.
    """
    def __init__(self, app):
        self.app = app
        self.extAppExe = None
        
    def taskStart(self, exporter, exportType):
        """
        This is called before any call to createContent() during an
        export task.
        An export task can be a single HTML page for
        preview or a single page or a set of pages for export.
        exporter -- Exporter object calling the handler
        exportType -- string describing the export type
        
        Calls to createContent() will only happen after a 
        call to taskStart() and before the call to taskEnd()
        """
        # Find Gnuplot executable by configuration setting
        self.extAppExe = self.app.getGlobalConfig().get("main",
                "plugin_pic2plot_exePath", "")
        
        self.imExtAppExe = self.app.getGlobalConfig().get("main",
                "plugin_imagemagick_dirPath", "")

        self.alwaysCrop = str(self.app.getGlobalConfig().get("main",
                "plugin_pic2plot_alwaysCrop", u"")) == "True"

    def taskEnd(self):
        """
        Called after export task ended and after the last call to
        createContent().
        """
        pass


    def createContent(self, exporter, exportType, insToken):
        """
        Handle an insertion and create the appropriate content.

        exporter -- Exporter object calling the handler
        exportType -- string describing the export type
        insToken -- insertion token to create content for (see also 
                PageAst.Insertion)

        An insertion token has the following member variables:
            key: insertion key (unistring)
            value: value of an insertion (unistring)
            appendices: sequence of strings with the appendices

        Meaning and type of return value is solely defined by the type
        of the calling exporter.
        
        For HtmlXmlExporter a unistring is returned with the HTML code
        to insert instead of the insertion.        
        """
        if not insToken.value:
            # Nothing in, nothing out
            return u""
        
        if self.extAppExe == "":
            # No path to pic2plot executable -> show message
            return u"<pre>" + _(u"[Please set path to pic2plot executable]") +\
                    u"</pre>"

        if self.imExtAppExe == "":
            hasImageMagick = False
        else:
            hasImageMagick = True

        crop = False

        # Get exporters temporary file set (manages creation and deletion of
        # temporary files)
        tfs = exporter.getTempFileSet()

        pythonUrl = (exportType != "html_previewWX")

        dstFullPath = tfs.createTempFile("", ".png", relativeTo="")

        url = tfs.getRelativeUrl(None, dstFullPath, pythonUrl=pythonUrl)
        
        baseDir = os.path.dirname(exporter.getMainControl().getWikiConfigPath())

        # find arguments in appendices
        pattern = 'args='
        arguments = ''
        for a in insToken.appendices:
            if a.startswith(pattern):
                arguments = str(a[len(pattern):])
            if a == 'crop' and hasImageMagick:
                crop = True

        # Retrieve quoted content of the insertion
        #bstr = lineendToOs(mbcsEnc(insToken.value, "replace")[0])
        # windows lineendings confuse pic2plot
        bstr = mbcsEnc(insToken.value, "replace")[0]

        # Store token content in a temporary file
        srcfilepath = createTempFile(bstr, ".pic")
        try:
            cmdline = '%s %s -T png %s > "%s"' % (self.extAppExe, arguments, srcfilepath, dstFullPath)
            
            # Run external application
            from subprocess import Popen, PIPE
            p = Popen(cmdline, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=False)
            p.wait() # Will wait for process to end
            childIn, childOut, childErr = (p.stdin, p.stdout, p.stderr)

            cropcmd = ''
            if crop or self.alwaysCrop:
                cropcmd = '%s -trim "%s" "%s"' % (os.path.join(self.imExtAppExe, 'convert'), dstFullPath, dstFullPath)
                p2 = Popen(cropcmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=False)
                p2.wait()
                childIn2, childOut2, childErr2 =(p2.stdin, p2.stdout, p2.stderr) 

            if u"noerror" in [a.strip() for a in insToken.appendices]:
                childErr.read()
                errResponse = ""
            else:
                errResponse = childErr.read()
        finally:
            os.unlink(srcfilepath)
            
        # Open logfile
        if LOG:
            f = open(baseDir + '\pic2plot.log', 'a')
            f.write(time.strftime('\n%d-%m-%Y %H:%M:%S\n'))
            f.write("Current dir: %s\n" % os.getcwd())
            f.write("Commandline: %s\n" % cmdline)
            f.write(time.strftime('\n%d-%m-%Y %H:%M:%S') + ' : ' + errResponse)
            f.write("Crop: %s\n" % cropcmd)
            for a in insToken.appendices:
                appendices = a.strip().split("---")
                for app in appendices:
                    f.write(app.strip() + "\n")
            f.close()

        if errResponse != "":
            errResponse = mbcsDec(errResponse, "replace")[0]
            return u"<pre>" + _(u"[pic2plot error: %s]") % errResponse +\
                    u"</pre>"

        # Return appropriate HTML code for the image
        if exportType == "html_previewWX":
            # Workaround for internal HTML renderer
            return (u'<img src="%s" border="0" align="bottom" alt="pic2plot" />'
                    u'&nbsp;') % url
        else:
            return u'<img src="%s" border="0" align="bottom" alt="pic2plot" />' \
                    % url


    def getExtraFeatures(self):
        """
        Returns a list of bytestrings describing additional features supported
        by the plugin. Currently not specified further.
        """
        return ()
        

def registerOptions(ver, app):
    """
    API function for "Options" plugins
    Register configuration options and their GUI presentation
    ver -- API version (can only be 1 currently)
    app -- wxApp object
    """
    # Register option
    app.getDefaultGlobalConfigDict()[("main", "plugin_pic2plot_exePath")] = u""
    app.getDefaultGlobalConfigDict()[("main", "plugin_imagemagick_dirPath")] = u""
    app.getDefaultGlobalConfigDict()[("main", "plugin_pic2plot_alwaysCrop")] = u"False"

    # Register panel in options dialog
    app.addOptionsDlgPanel(Pic2plotOptionsPanel, u"  pic2plot")


class Pic2plotOptionsPanel(wx.Panel):
    def __init__(self, parent, optionsDlg, app):
        """
        Called when "Options" dialog is opened to show the panel.
        Transfer here all options from the configuration file into the
        text fields, check boxes, ...
        """
        wx.Panel.__init__(self, parent)
        self.app = app
        
        pt = self.app.getGlobalConfig().get("main", "plugin_pic2plot_exePath", "")
        
        self.tfPath = wx.TextCtrl(self, -1, pt)

        mainsizer = wx.BoxSizer(wx.VERTICAL)

        inputsizer = wx.BoxSizer(wx.HORIZONTAL)
        inputsizer.Add(wx.StaticText(self, -1, _(u"Path to pic2plot:")), 0,
                wx.ALL | wx.EXPAND, 5)
        inputsizer.Add(self.tfPath, 1, wx.ALL | wx.EXPAND, 5)
        mainsizer.Add(inputsizer, 0, wx.EXPAND)
        

        pt = self.app.getGlobalConfig().get("main", "plugin_imagemagick_dirPath", "")
        self.ifPath = wx.TextCtrl(self, -1, pt)

        inputsizer = wx.BoxSizer(wx.HORIZONTAL)
        inputsizer.Add(wx.StaticText(self, -1, _(u"Path to ImageMagick folder:")), 0,
                wx.ALL | wx.EXPAND, 5)
        inputsizer.Add(self.ifPath, 1, wx.ALL | wx.EXPAND, 5)
        mainsizer.Add(inputsizer, 0, wx.EXPAND)

        # add alwaysCrop checkbox
        self.alwaysCropCB = wx.CheckBox(self, -1, 'Always crop output images:')
        self.alwaysCropCB.SetToolTip(wx.ToolTip(
            "You will need ImageMagick for this"))
        pt = self.app.getGlobalConfig().get("main",
                "plugin_pic2plot_alwaysCrop", u"")

        pt = (str(pt) == "True")
        self.alwaysCropCB.SetValue(pt)
        mainsizer.Add(self.alwaysCropCB, 0, wx.ALL | wx.EXPAND, 5)
        self.alwaysCropCB.Bind(wx.EVT_CHECKBOX, self.clickedCB)

        self.SetSizer(mainsizer)
        self.Fit()

    def clickedCB(self, event):
        if not self.ifPath.GetValue():
            wx.SafeShowMessage( "Error",
                    """
                    You need to set the path to ImageMagick to crop images.
                    """)

    def setVisible(self, vis):
        """
        Called when panel is shown or hidden. The actual wxWindow.Show()
        function is called automatically.
        
        If a panel is visible and becomes invisible because another panel is
        selected, the plugin can veto by returning False.
        When becoming visible, the return value is ignored.
        """
        return True

    def checkOk(self):
        """
        Called when "OK" is pressed in dialog. The plugin should check here if
        all input values are valid. If not, it should return False, then the
        Options dialog automatically shows this panel.
        
        There should be a visual indication about what is wrong (e.g. red
        background in text field). Be sure to reset the visual indication
        if field is valid again.
        """
        return True

    def handleOk(self):
        """
        This is called if checkOk() returned True for all panels. Transfer here
        all values from text fields, checkboxes, ... into the configuration
        file.
        """
        pt = self.tfPath.GetValue()
        self.app.getGlobalConfig().set("main", "plugin_pic2plot_exePath", pt)

        pt = self.ifPath.GetValue()
        self.app.getGlobalConfig().set("main", "plugin_imagemagick_dirPath", pt)

        pt = self.alwaysCropCB.GetValue()
        self.app.getGlobalConfig().set("main",
                "plugin_pic2plot_alwaysCrop", str(pt))

Attachments