Difference between revisions of "InkSlide"

From Inkscape Wiki
Jump to navigation Jump to search
m
 
(18 intermediate revisions by one other user not shown)
Line 8: Line 8:


<pre>
<pre>
%slide InkSlide: Features
InkSlide: Features
++++++++++++++++++
 
Features include wrapped top level text and
Features include wrapped top level text and
    mulitple
- mulitple
        levels
  - levels
            of wrapped bulleted lists with bullets and font
    - of wrapped bulleted lists with bullets and font
information taken from the template file.
      information taken from the template file.


Slide specific content like this:
Slide specific content like this:
Line 27: Line 29:
=== Download ===
=== Download ===


Copy the [[InkSlide#Code|text below]] into a file called 'inkslide.py'.
Copy the [[InkSlide#Code|text below]] into a file called 'inkslide.py'. (How do you attach a non-image file on this wiki?)


=== Use ===
Here is an [[Media:template0.svg|example template]]. Use Save Link As to stop your browser displaying the file instead of downloading it.
 
<pre>python inkslide.py myTemplate.svg mySlides.txt</pre>
 
This creates a slide00xx.svg for each slide in your presentation.
 
<pre>python inkslide.py --writeInstance <instanceId> myTemplate.svg mySlides.txt</pre>
 
Creates / updates the Inkscape file you can edit to add special content to this particular slide.  '''NOT IMPLEMENTED YET''' - the code exists, but not the command line hook.


=== How it works ===
=== How it works ===
Line 45: Line 39:
InkSlide saves the file, and uses Inkscape's command line ''--query-height'' and friends to find out how high the wrapped text pieces are.  At it then repositions them properly, adds clones of the bullet markers, etc.  Bullet markers have special @id attributes like ''bu_1'' for the first level of bulleted list.
InkSlide saves the file, and uses Inkscape's command line ''--query-height'' and friends to find out how high the wrapped text pieces are.  At it then repositions them properly, adds clones of the bullet markers, etc.  Bullet markers have special @id attributes like ''bu_1'' for the first level of bulleted list.


Finally, InkSlide checks to see if there's an %id tag for the slide, and if so if there's a corresponding Inkscape SVG file with content for this particular slide. This content has to occur in layers whose name starts with ''Instance'', and these layers have to be present in the template file, but you can have more than one, so they can be above and below the regular content.  If such content is found, it's copied into the output slide.
Finally, InkSlide checks to see if there's an id tag for the slide, and if so if there's a corresponding Inkscape SVG file with content for this particular slide. This content has to occur in layers whose name starts with ''Instance'', and these layers have to be present in the template file, but you can have more than one, so they can be above and below the regular content.  If such content is found, it's copied into the output slide.


=== The Template File ===
=== The Template File ===
Line 53: Line 47:
Required contents:
Required contents:


*A layer called ''Texts'', into which text will be placed.
*A group or layer called ''gr_text'', into which text will be placed.
*A flowed text frame with an @id of ''fr_title'' to define the font and position for the slide title.  This should contain text like ''Title goes here Sly'' because ''Sly'' includes letters which ascend and descend from the baseline to the greatest extent.  The placeholder text must be '''only''' one line because it's used to calculate line height.
*A flowed text frame with an @id of ''fr_title'' to define the font and position for the slide title.  This should contain text like ''Title goes here Sly'' because ''Sly'' includes letters which ascend and descend from the baseline to the greatest extent.  The placeholder text must be '''only''' one line because it's used to calculate line height.
*A group with an @id of ''gr_title'' into which the title will be placed.  This makes it easier to do special effects on the title like shadows.
*A group or layer called ''gr_title'' into which the title will be placed.  This makes it easier to do special effects on the title like shadows.
*Flowed text frames with an @id of ''fr_tabN'' for each N in 0, 1, 2, 3.  ''fr_tab0'' is used for top level non bulleted text.  These should also include ''Sly'' or similar.  The placeholder text must be '''only''' one line because it's used to calculate line height.
*Flowed text frames with an @id of ''fr_tabN'' for each N in 0, 1, 2, 3.  ''fr_tab0'' is used for top level non bulleted text.  These should also include ''Sly'' or similar.  The placeholder text must be '''only''' one line because it's used to calculate line height.
*Objects or groups withan @id of ''bu_N'' for each N in 1, 2, 3, for the three levels of bulleted list.
*Objects or groups withan @id of ''bu_N'' for each N in 1, 2, 3, for the three levels of bulleted list.
Line 61: Line 55:
The fr_* elements define the font, font-size, color etc. for the different levels of text.  They also define the position and wrap width.  See the example template file for a possible arrangement.
The fr_* elements define the font, font-size, color etc. for the different levels of text.  They also define the position and wrap width.  See the example template file for a possible arrangement.


"A group or layer called 'X'" indicates either a group with an '''@id'''
of 'X' ''or'' a layer with a '''label''' (name) of 'X'.  When both exist the group will take precedence.
Optional contents:
Optional contents:


*A background
*A background, which should be placed in a group or layer called `gr_bg`
*Affiliation text and logos you want to appear on every slide
*A foreground, which should be placed in a group or layer called `gr_fg`
*A blurred clone of the gr_title element, offset and behind it.
*Affiliation text and logos you want to appear on every slide, this could go in the background, or another named group or layer.
*A blurred clone of the gr_title element, offset and behind it.  It's easier to do this is `gr_title` is a group rather than a layer.
*Layers whose name starts with ''Instance'' - these are the layers the user can edit directly in Inkscape for a particular slide.
*Layers whose name starts with ''Instance'' - these are the layers the user can edit directly in Inkscape for a particular slide.


When you save the template file you should make sure that layers containing place holder text (the fr_* elements) and bullets are invisible, or at least hidden below the background layer, which should be visible.  Likewise the Texts layer should be visible, as well as any Instance layers.
When you save the template file you should make sure that layers containing place holder text (the fr_* elements) and bullets are invisible.  Likewise the Texts layer should be visible, as well as any Instance layers.
=== Text Input ===


=== Text Input ===
InkSlide uses [http://docutils.sourceforge.net/rst.html reStructuredText] for markup, although it renders only a subset of rst.


<pre>
<pre>
%title Inkslide: an example
Inkslide: an example
++++++++++++++++++++
 
This text will appear as a top level paragraph.
This text will appear as a top level paragraph.
    Indented by a single tab, this text will appear as a bulleted item.
        Two tabs -> second level bulleted list.


A gap will be left for the preceeding line.
- Dash prefix indicates this text will appear as a bulleted item.
%pause
Two slides will be produced for this input, one with everything up to the %pause above, and one with the remaining content.  Incremental list display can be done this way.


The %id tag below indicates that a file with a name like 'extra/is_intro.svg' should be inspected for extra content for this particular slide.
  - Indented dash prefix -> second level bulleted list.
%id is_intro
 
%title Inkslide: the next slide...
.. is:pause
 
Two slides will be produced for this input, one with everything up to the
".. is:pause", and one with that plus the remaining content.
Incremental list display can be done this way.
 
The is:id tag below indicates that a file with a name like
'extra/is_intro.svg' should be inspected for extra content
for this particular slide.
 
.. is:id is_intro
 
Inkslide: the next slide...
+++++++++++++++++++++++++++
</pre>
</pre>


Line 95: Line 105:
=== To do / status ===
=== To do / status ===


InkSlide is very usable, but is missing some features:
InkSlide is very usable, but is missing some features.  Remember you can
use the slide specific editing feature to do any of these things and
anything else you can do in Inkscape already, these will just be more
convenient in the text input when they're done.


*Inline text formatting (bold, color, etc.) for part of a piece of text
*Inline text formatting (bold, color, etc.) for part of a piece of text
*Justification switching
*Justification switching
*Verbatim code display
*Verbatim code display
*Simple image inclusion
*<strike>Simple image inclusion</strike>
**currently you can do this using the Inkscape editing features
*more helper tools to make PNG and PDF versions of slides
*more helper tool to make PNG and PDF versions of slides
*configuring / using a directory for edited slides
*configuring / using a directory for edited slides
*command line flags for generating specific slides
*command line flags for generating specific slides
Currently InkSlide is slow, because it must invoke Inkscape once for every single dimension it needs to retrieve.  I'll try and get a --query-all switch into 0.46.


=== Bugs ===
=== Bugs ===
Line 115: Line 129:


InkSlide was inspired by [http://member.wide.ad.jp/wg/mgp/ MagicPoint]
InkSlide was inspired by [http://member.wide.ad.jp/wg/mgp/ MagicPoint]
Matt Harrison suggested using reStructured text as the markup language.


=== Code ===
=== Code ===


<pre>
<pre>
"""
"""
# inkslide.py $Id$
# inkslide.py $Id$
# Author: Terry Brown
# Author: Terry Brown
Line 128: Line 147:
import subprocess
import subprocess
import os.path
import os.path
from docutils.core import publish_string
import sys  # for debug
class inkSlide(object):
    def __init__(self):
        self.NS = { 'i': 'http://www.inkscape.org/namespaces/inkscape',
          's': 'http://www.w3.org/2000/svg',
          'xlink' : 'http://www.w3.org/1999/xlink'}


NS = { 'i': 'http://www.inkscape.org/namespaces/inkscape',
        # number of defined tablevels, FIXME, could derive from template?
      's': 'http://www.w3.org/2000/svg',
        self.tabLevels = 4 
      'xlink' : 'http://www.w3.org/1999/xlink'}


# number of defined tablevels, FIXME, could derive from template?
         self._nextId = 0
tabLevels = 4 
def clearId(x, what='id', NS={}):
    """recursively clear @id on element x and descendants"""
    if what in x.keys():
         del x.attrib[what]
    for i in x.xpath('.//*[@%s]'%what, NS):
        del i.attrib[what]
    return x
def getDim(fn, Id, what):
    """return dimension of element in fn with @id Id, what is
    x, y, width, height
    """
    cmd = ('inkscape', '--without-gui', '--query-id', Id, '--query-'+what, fn)
    proc = subprocess.Popen(cmd, stdout = subprocess.PIPE,
                            stderr = subprocess.PIPE)
    # make new pipe for stderr to supress chatter from inkscape
    proc.wait()
    return proc.stdout.read()
def textReader(text):
    """iterate parts in input text"""
    if not text: return
    for line in text.split('\n'):
        if line.startswith('%slide '):
            yield {'type':'TITLE', 's':line[7:].strip()}
            continue
        if line.startswith('%pause'):
            yield {'type':'PAUSE'}
            continue
        if line.startswith('%id '):
            yield {'type':'ID', 's':line[4:].strip()}
            continue
        if not line.strip():
            yield {'type':'BLANK'}
            continue
        tabs = len(line) - len(line.lstrip('\t'))
        yield {'type':'TAB', 'tabs':tabs, 's':line.strip()}


nid = 0
        self.gap = {
def nextId():
            'bIMAGE': 15,  # gap before image
    """return an unsed Id"""
            'aIMAGE': 15,  # gap after image
    # FIXME, should inspect templat doc.
            'aTAB0': 10,    # gap after top level text
    global nid
            'aTAB1': 10,    # gap after level 1 bullet
    nid += 1
            'aTAB2': 10,   # gap after level 2 bullet
    return 'is'+str(nid)
            'aTAB3': 10,    # gap after level 3 bullet
            'fVCENTER': 1,  # flag - center vertically
            'dHEIGHT': 728, # dimension, bottom of page
            'fHCENTER': 1,  # flag - center vertically
            'dWIDTH': 1024, # dimension, bottom of page
        }


def instanceFromTemplate(template, instance, text, pauseOk = True):
        self.dimCache = {}
        self.is_reads, self.is_cache = 0, 0
    def __del__(self):
        print '%d reads, %d cache hits' % (self.is_reads, self.is_cache)
    def nextId(self):
        """return an unsed Id"""
        # FIXME, should inspect template doc.
        self._nextId += 1
        return 'is'+str(self._nextId)


     slide = 0
     def clearId(self, x, what='id', NS={}):
        """recursively clear @id on element x and descendants"""
        if not NS: NS = self.NS
        if what in x.keys():
            del x.attrib[what]
        for i in x.xpath('.//*[@%s]'%what, NS):
            del i.attrib[what]
        return x
    def getDim(self, fn, Id, what):
        """return dimension of element in fn with @id Id, what is
        x, y, width, height
        """


     hlist = []
        hsh = fn+Id+what
    doc0 = ET.parse(template)
        if hsh in self.dimCache:
    slideId = None
            self.is_cache += 1
   
            return self.dimCache[hsh]
    for t in textReader(text):
 
       
        cmd = ('inkscape', '--without-gui', '--query-id', Id, '--query-'+what, fn)
        if t['type'] == 'ID':
 
            slideId = t['s']
        proc = subprocess.Popen(cmd, stdout = subprocess.PIPE,
            continue
                                stderr = subprocess.PIPE)
              
        # make new pipe for stderr to supress chatter from inkscape
        if t['type'] == 'TITLE':
        proc.wait()
            print t['s']
        self.dimCache[hsh] = proc.stdout.read()
            if hlist:
        if self.dimCache[hsh].strip() == '':
                process(instance, doc0, hlist, slideId, slide)
            print "Warning: '%s' not found" % Id
                slide = slide + 1
        self.is_reads += 1
                hlist = [t]
        return self.dimCache[hsh]
                doc0 = ET.parse(template)
    def textReader(self, text):
                 slideId = None
        """NOT USED iterate parts in input text"""
             else:
        if not text: return
        for line in text.split('\n'):
            if line.startswith('%slide '):
                yield {'type':'TITLE', 's':line[7:].strip()}
                continue
            if line.startswith('%pause'):
                yield {'type':'PAUSE'}
                continue
            if line.startswith('%id '):
                yield {'type':'ID', 's':line[4:].strip()}
                continue
            if not line.strip():
                yield {'type':'BLANK'}
                continue
            tabs = len(line) - len(line.lstrip('\t'))
            yield {'type':'TAB', 'tabs':tabs, 's':line.strip()}
 
    def textReaderRst(self, text):
 
        xml = publish_string(text.replace('\t','    '), writer_name = 'xml')
        doc = ET.fromstring(xml)
        # yield {'type':'TITLE', 's':doc.xpath('/document/@title')[0]}
        nodes = 'paragraph', 'comment', 'title', 'image'
        for i in doc.xpath('//%s' % '|//'.join(nodes)):
 
            if i.tag == 'title':
                yield {'type':'TITLE', 's':i.text}
            if i.tag == 'paragraph':
                tabs = len(i.xpath('ancestor::list_item'))
                yield {'type':'TAB', 'tabs':tabs, 's':i.text, 'e':i}
            if i.tag == 'image':
                tabs = len(i.xpath('ancestor::list_item'))
                yield {'type':'IMAGE', 'uri':i.get('uri')}
            if i.tag == 'comment':
 
                comparts = i.text.split(None, 1)
 
                if len(comparts) > 1:
                    ctag, cval = comparts
                elif len(comparts) == 1:
                    ctag = comparts[0]
                else:
                    continue  # empty comment had no meaning to InkView
 
                if ctag == 'is:id':
                    try:
                        yield {'type':'ID', 's':cval}
                    except:
                        raise Exception('"id" comment requires a value')
                elif ctag == 'is:notitle':
                    yield {'type':'DROP', 'what':('gr_title',)}
                elif ctag == 'is:notext':
                    yield {'type':'DROP', 'what':('gr_text',)}
                elif ctag == 'is:nobackground':
                    yield {'type':'DROP', 'what':('gr_bg',)}
                elif ctag == 'is:noforeground':
                    yield {'type':'DROP', 'what':('gr_fg',)}
                elif ctag == 'is:blank':
                    yield {'type':'DROP', 'what':('gr_title','gr_text')}
                elif ctag == 'is:empty':
                    yield {'type':'DROP', 'what':('gr_bg','gr_title','gr_text','gr_fg')}
                elif ctag == 'is:no':
                    try:
                        yield {'type':'DROP', 'what':(cval,)}
                    except:
                        raise Exception('"no" comment requires a value')
 
                if ctag == 'is:dim':
                    dim, val = cval.strip().split()
                    yield {'type':'DIM', 'dim':dim, 'val':val}
    def groupOrLayer(self, g):
        dst = self.doc.xpath('//s:g[@id="%s"]'%g, self.NS)
        if not dst: dst = self.doc.xpath('//s:g[@i:label="%s"]'%g, self.NS)
        if not dst: return dst  # let the caller test, don't raise exception here
        dst = dst[0]
        return dst
     def instanceFromTemplate(self, template, instance, text, pauseOk = True):
 
        slide = 0
 
        hlist = []
        self.doc = ET.parse(template)
        slideId = None
 
        for t in self.textReaderRst(text):
 
            if t['type'] == 'ID':
                slideId = t['s']
                continue
 
             if t['type'] == 'TITLE':
                if hlist:
                    self.process(instance, hlist, slideId, slide)
                    slide = slide + 1
                    hlist = [t]
                    self.doc = ET.parse(template)
                    slideId = None
                else:
                    hlist.append(t)
 
                print t['s']
 
                dst = self.groupOrLayer('gr_title')
                 src = self.doc.xpath('//s:flowRoot[@id="fr_title"]', self.NS)[0]
                cpy = self.clearId(copy.deepcopy(src))
                Id = self.nextId()
                cpy.set('id', Id)
                hlist[-1]['id']=Id
                cpy.xpath('s:flowPara',self.NS)[0].text = t['s']
                dst.append(cpy)
                continue
 
            if t['type'] == 'PAUSE':
                if pauseOk:
                    self.process(instance, hlist, slideId, slide)
                    slide = slide + 1
                continue
 
             if t['type'] == 'BLANK':
                 hlist.append(t)
                 hlist.append(t)
            dst = doc0.xpath('//s:g[@id="gr_title"]', NS)[0]
                continue
            src = doc0.xpath('//s:flowRoot[@id="fr_title"]', NS)[0]
            cpy = clearId(copy.deepcopy(src))
            Id = nextId()
            cpy.set('id', Id)
            hlist[-1]['id']=Id
            cpy.xpath('s:flowPara',NS)[0].text = t['s']
            dst.append(cpy)
            continue


        if t['type'] == 'PAUSE':
            if t['type'] == 'TAB':
            if pauseOk:
                hlist.append(t)
                 process(instance, doc0, hlist, slideId, slide)
                dst = self.groupOrLayer('gr_text')
                 slide = slide + 1
                src = self.doc.xpath('//s:flowRoot[@id="fr_tab%d"]' % t['tabs'], self.NS)[0]
            continue
                cpy = self.clearId(copy.deepcopy(src))
                Id = self.nextId()
                 cpy.set('id', Id)
                hlist[-1]['id']=Id
                # cpy.xpath('s:flowPara',self.NS)[0].text = t['s']
                self.flowParaFromElement(
                    cpy.xpath('s:flowPara',self.NS)[0], t['e'])
                 dst.append(cpy)
                continue


        hlist.append(t)
            if t['type'] == 'IMAGE':
                hlist.append(t)
                dst = self.groupOrLayer('gr_image')
                if not dst:
                    dst = self.groupOrLayer('gr_text')
                img = ET.Element('image')
                img.set('{%s}href'%self.NS['xlink'], t['uri'])
                Id = self.nextId()
                img.set('id', Id)
                hlist[-1]['id']=Id
                dst.append(img)
                continue


        if t['type'] == 'BLANK':
             hlist.append(t) # whatever it was
            continue
        if t['type'] == 'TAB':
            dst = doc0.xpath('//s:g[@i:label="Texts"]', NS)[0]
            src = doc0.xpath('//s:flowRoot[@id="fr_tab%d"]' % t['tabs'], NS)[0]
            cpy = clearId(copy.deepcopy(src))
            Id = nextId()
            cpy.set('id', Id)
             hlist[-1]['id']=Id
            cpy.xpath('s:flowPara',NS)[0].text = t['s']
            dst.append(cpy)
            #break
            continue
         
    process(instance, doc0, hlist, slideId, slide)
def process(instance, doc0, hlist, slideId, slide):


     instance = instance.replace('%s', '%04d'%slide)
        self.process(instance, hlist, slideId, slide)
     doc0.write(file(instance, 'w'))
     def flowParaFromElement(self, fp, e):
        fp.text = e.xpath('string(.)')
     def process(self, instance, hlist, slideId, slide):


    bullDim = {}
         instanceName = instance.replace('%s', '%04d'%slide)
    tabDim = {}
    for n in range(0,tabLevels):
        b = {}
         x = doc0.xpath('//*[@id="bu_%d"]'%n, NS)
        if x:
            # clearId(x[0], what='transform')
            for i in 'x', 'y', 'width', 'height':
                b[i] = float(getDim(instance, 'bu_%d'%n, i))
            bullDim[n] = b
        b = {}
        x = doc0.xpath('//*[@id="fr_tab%d"]'%n, NS)
        if x:
            # clearId(x[0], what='transform')
            for i in 'x', 'y', 'height':
                b[i] = float(getDim(instance, 'fr_tab%d'%n, i))
            tabDim[n] = b


    delta = 0.
        instance = 'tmp.svg'  # reusing allows caching in getDim, but relies on nextId ensuring
    firstVert = True
                              # that the changing parts have new ids
    for n, i in enumerate(hlist):
        if hlist[n]['type'] in ['TITLE']:
            continue # already in right place, not a vertical element


         hgt = 0
         self.doc.write(file(instance, 'w'))
        if hlist[n-1]['type'] == 'BLANK':
 
            hgt = 30
        drop = []
        else:
 
            hgt = float(getDim(instance, hlist[n-1]['id'], 'height'))
        bullDim = {}
        tabDim = {}
        for n in range(0,self.tabLevels):
            b = {}
            x = self.doc.xpath('//*[@id="bu_%d"]'%n, self.NS)
            if x:
                # self.clearId(x[0], what='transform')
                for i in 'x', 'y', 'width', 'height':
                    b[i] = float(self.getDim(instance, 'bu_%d'%n, i))
                bullDim[n] = b
            b = {}
            x = self.doc.xpath('//*[@id="fr_tab%d"]'%n, self.NS)
            if x:
                # clearId(x[0], what='transform')
                for i in 'x', 'y', 'width', 'height':
                    b[i] = float(self.getDim(instance, 'fr_tab%d'%n, i))
                x = x[0].xpath('.//s:rect',self.NS)[0]
                b['max-width'] = float(x.get('width'))
                b['max-height'] = float(x.get('height'))
                tabDim[n] = b
 
        delta = 0.
        firstVert = True
        placed_xy = []
        placed_transform = []
 
        for n, i in enumerate(hlist):
 
            this = hlist[n]
            prev = hlist[n-1]
 
            if this['type'] == 'DROP': drop += this['what']
 
            if this['type'] == 'DIM':
                if this['dim'] not in self.gap or this['val'][0] not in '+-':
                    self.gap[this['dim']] = float(this['val'])
                else:
                    self.gap[this['dim']] += float(this['val'])
 
            zeroHeight = ['TITLE', 'DROP', 'DIM']


        hgt += 5
            # don't do this - need to add height from prev to delta
            #if this['type'] in zeroHeight:
            #    continue  # already in right place, not a vertical element


        if firstVert:
             hgt = 0
             hgt = 0
             firstVert = False
             if prev['type'] == 'BLANK':
                hgt = 30
            elif prev['type'] not in zeroHeight:
                hgt = float(self.getDim(instance, prev['id'], 'height'))
 
                # risky gambit to space bullets properly when some lines
                # have no ascenders etc.
                if prev['type'] == 'TAB' and prev['tabs'] > 0:
                    lnh = tabDim[prev['tabs']]['height']
                    hgt = lnh * int(hgt/lnh+0.5)
 
            # TAB0, TAB1, TITLE, IMAGE, etc.
            # gap after previous
            hsh = 'a' + prev['type'] + str(prev.get('tabs',''))
            hgt += self.gap.get(hsh, 0)
            # gap before current
            hsh = 'b' + this['type'] + str(this.get('tabs',''))
            hgt += self.gap.get(hsh, 0)
 
            if firstVert:
                hgt = 0
                firstVert = False


        delta += hgt
            delta += hgt


        if hlist[n]['type'] == 'BLANK': continue
            if this['type'] == 'BLANK': continue


        tabs = hlist[n]['tabs']
            if this['type'] == 'TAB':
                tabs = this['tabs']


        r = doc0.xpath('//s:flowRoot[@id="%s"]' % hlist[n]['id'], NS)[0]
                r = self.doc.xpath('//s:flowRoot[@id="%s"]' % this['id'], self.NS)[0]
        clearId(r, what='transform')
                self.clearId(r, what='transform')
        r = r.xpath('.//s:rect', NS)[0]
                r = r.xpath('.//s:rect', self.NS)[0]
        r.set('x', str(tabDim[tabs]['x']))
                r.set('x', str(tabDim[tabs]['x']))
        y = tabDim[tabs]['y'] + delta
                y = tabDim[tabs]['y'] + delta
        #print '%s -> %s' % (str(r.get('y')), str(y))
                #print '%s -> %s' % (r.get('y'), y)
        #print tabDim[tabs]['y']
                #print tabDim[tabs]['y']
        r.set('y', str(y))
                r.set('y', str(y))
                placed_xy.append([r, this['id']])


        if (hlist[n]['type'] == 'TAB' and hlist[n]['tabs'] in bullDim
            if (this['type'] == 'TAB' and this['tabs'] in bullDim
            and 'isDone' not in hlist[n]):
                and 'isDone' not in this):
            hlist[n]['isDone'] = True
                this['isDone'] = True
            dx = (tabDim[tabs]['x']  
                dx = (tabDim[tabs]['x']  
                  - bullDim[tabs]['x']
                      - bullDim[tabs]['x']
                  - 1.5*bullDim[tabs]['width']
                      - 1.5*bullDim[tabs]['width']
                  )
                      )
            dy = (y  
                dy = (y  
                  + tabDim[tabs]['height'] / 2.
                      + tabDim[tabs]['height'] / 2.
                  - bullDim[tabs]['y']  
                      - bullDim[tabs]['y']  
                  - bullDim[tabs]['height'] / 2.
                      - bullDim[tabs]['height'] / 2.
                  )
                      )
            dst = doc0.xpath('//s:g[@i:label="Texts"]', NS)[0]
                # dst = doc0.xpath('//s:g[@i:label="Texts"]', NS)[0]
            #print y, tabDim[tabs]['height'], bullDim[tabs]['y'], bullDim[tabs]['height'], dy
                dst = self.groupOrLayer('gr_text')
            clone = ET.Element('svg:use')
                #print y, tabDim[tabs]['height'], bullDim[tabs]['y'], bullDim[tabs]['height'], dy
            clone.set('transform', 'translate(%f,%f)' % (dx,dy))
                clone = ET.Element('use')
            clone.set('{%s}href'%NS['xlink'], '#bu_%d'%tabs)
                clone.set('transform', 'translate(%f,%f)' % (dx,dy))
            dst.append(clone)
                clone.set('{%s}href'%self.NS['xlink'], '#bu_%d'%tabs)
           
                placed_transform.append([clone, dx, dy])
            #path = ET.Element('svg:path')
                dst.append(clone)
            #path.set('d', ('M %f,%f' + ' L %f,%f'*5 + ' z') % (
            #    bullDim[tabs]['x'], bullDim[tabs]['y'],
            #    bullDim[tabs]['x']+20, bullDim[tabs]['y']+bullDim[tabs]['height']/2.,
            #    bullDim[tabs]['x'], bullDim[tabs]['y']+bullDim[tabs]['height'],
            #    tabDim[tabs]['x'], tabDim[tabs]['y'],
            #    tabDim[tabs]['x']+20, tabDim[tabs]['y']+tabDim[tabs]['height']/2.,
            #    tabDim[tabs]['x'], tabDim[tabs]['y']+tabDim[tabs]['height'],
            #    ))
            #dst.append(path)
            #break


    # look for instance specific parts
             if this['type'] == 'IMAGE':
    if slideId:
        f = instancePath(slideId)
        if os.path.isfile(f):
            comp = ET.parse(f)
             svg = doc0.xpath('//s:svg', NS)[0]
            g = "{%s}g"%NS['s']
            k = "{%s}label"%NS['i']
            for n, i in enumerate(svg.getchildren()):
                if i.tag == g and i.get(k, '').startswith('Instance'):
                    x = comp.xpath('//s:svg//s:g[@i:label="%s"]'%i.get(k), NS)
                    if x:
                        x = x[0]
                        cpy = clearId(copy.deepcopy(x))
                        svg[n] = cpy
            defs = comp.xpath('//s:svg/s:defs/*', NS)
            dst = doc0.xpath('//s:svg/s:defs', NS)[0]
            for d in defs:
                Id = d.get('id')
                current = doc0.xpath('//s:svg/s:defs/*[@id="%s"]'%Id, NS)
                if not current:
                    cpy = copy.deepcopy(d)
                    dst.append(cpy)


    doc0.write(file(instance, 'w'))
                xpath = '//s:image[@id="%s"]' % this['id']  # doesn't work?
   
                xpath = '//*[@id="%s"]' % this['id']
    return slideId
                i = self.doc.xpath(xpath, self.NS)[0]
def instancePath(slideId, component = None):
                w = float(self.getDim(instance, this['id'], 'width'))
    """return path to component for slide"""
                x = tabDim[0]['x']+tabDim[0]['max-width']/2-w/2
    # return os.path.join(slideId, component)
                i.set('x', str(x))
    return slideId+'.svg'
                y = tabDim[0]['y'] + delta
def writeComponents(inkscapeFile, slideId):
                i.set('y', str(y))
    """write components from file for slide"""
                placed_xy.append([i,this['id']])
    if not slideId: return
 
    doc0 = ET.parse(inkscapeFile)
        if self.gap['fVCENTER']:
    hasComponents = False
 
    for i in doc0.xpath('//s:svg//s:g', NS):
            if 'id' in this:
        k = "{%s}label"%NS['i']
                delta += float(self.getDim(instance, this['id'], 'height'))
        # print i.keys(),k
 
        if (k in i.keys() and i.get(k).startswith('Instance')):
            recenter = (self.gap['dHEIGHT'] - delta - tabDim[0]['y']) * 0.45
            if len(i) > 0:
 
                hasComponents = True
            for i in placed_transform:
                break
                i[0].set('transform', 'translate(%f,%f)'%(i[1],i[2]+recenter))
                # ET.ElementTree(i).write(f)
                i[2] += recenter  # so it's not lost below
               
            for i in placed_xy:
    f = instancePath(slideId, 'components')
                i[0].set('y', str(float(i[0].get('y'))+recenter))
   
 
    if hasComponents:
        if self.gap['fHCENTER']:
        if (os.path.abspath(os.path.realpath(inkscapeFile)) !=  
 
            os.path.abspath(os.path.realpath(f))):
            maxX = 0
            file(f, 'w').write(file(inkscapeFile).read())
            for i in placed_xy:
    else:
                x = float(self.getDim(instance, i[1], 'x'))
        if os.path.isfile(f):
                x += float(self.getDim(instance, i[1], 'width'))
            os.remove(f)
                if x > maxX: maxX = x
 
            recenter = (self.gap['dWIDTH'] - maxX - tabDim[0]['x']) * 0.5
 
            for i in placed_transform:
                i[0].set('transform', 'translate(%f,%f)'%(i[1]+recenter,i[2]))
                i[1] += recenter  # in case it's used again
            for i in placed_xy:
                if i[0].tag != 'image':  # images already centered
                    # print i[0].tag
                    i[0].set('x', str(float(i[0].get('x'))+recenter))
 
        # look for instance specific parts
        if slideId:
            f = self.instancePath(slideId)
            if os.path.isfile(f):
                comp = ET.parse(f)
                svg = self.doc.xpath('//s:svg', self.NS)[0]
                g = "{%s}g"%self.NS['s']
                k = "{%s}label"%self.NS['i']
                for n, i in enumerate(svg.getchildren()):
                    if i.tag == g and i.get(k, '').startswith('Instance'):
                        x = comp.xpath('//s:svg//s:g[@i:label="%s"]'%i.get(k), self.NS)
                        if x:
                            x = x[0]
                            cpy = self.clearId(copy.deepcopy(x))
                            # cpy = self.textCopy(x)
                            svg[n] = cpy
                defs = comp.xpath('//s:svg/s:defs/*', self.NS)
                dst = self.doc.xpath('//s:svg/s:defs', self.NS)[0]
                for d in defs:
                    Id = d.get('id')
                    current = self.doc.xpath('//s:svg/s:defs/*[@id="%s"]'%Id, self.NS)
                    if not current:
                        cpy = copy.deepcopy(d)
                        dst.append(cpy)
 
        for i in drop:
            e = self.groupOrLayer(i)
            if e:
                e.xpath('..')[0].remove(e)
 
        # problem losing xlink ns?
        for i in self.doc.xpath('//*[@href]'):
            i.set('{%s}href'%self.NS['xlink'], i.get('href'))
 
        self.doc.write(file(instanceName, 'w'))
 
        return slideId
    def textCopy(self, ele):
        """copy the element ele via xml->text->xml, as deepcopy seems to lose text"""
        txt = ET.tostring(ele)
        # print txt
        return ET.fromstring(txt)
    def instancePath(self, slideId):
        """return path to component for slide"""
        # return os.path.join(slideId, component)
        return slideId+'.svg'
    def writeComponents(self, inkscapeFile, slideId):
        """write components from file for slide"""
        if not slideId: return
        doc0 = ET.parse(inkscapeFile)
        hasComponents = False
        for i in doc0.xpath('//s:svg//s:g', self.NS):
            k = "{%s}label"%self.NS['i']
            # print i.keys(),k
            if (k in i.keys() and i.get(k).startswith('Instance')):
                if len(i) > 0:
                    hasComponents = True
                    break
                    # ET.ElementTree(i).write(f)
 
        f = self.instancePath(slideId)
 
        if hasComponents:
            if (os.path.abspath(os.path.realpath(inkscapeFile)) !=  
                os.path.abspath(os.path.realpath(f))):
                file(f, 'w').write(file(inkscapeFile).read())
        else:
            if os.path.isfile(f):
                os.remove(f)


if __name__ == '__main__':
if __name__ == '__main__':
     import sys
     import sys
     instanceFromTemplate(sys.argv[1], 'slide%s.svg', file(sys.argv[2]).read())
     x = inkSlide()
    x.instanceFromTemplate(sys.argv[1], 'slide%s.svg', file(sys.argv[2]).read())
 
</pre>
</pre>

Latest revision as of 00:19, 14 December 2010

InkSlide - quick and easy presentations using Inkscape

InkSlide produces slides like this:

Slide0004.jpg

from simple text input like this:

InkSlide: Features
++++++++++++++++++

Features include wrapped top level text and
- mulitple
  - levels
    - of wrapped bulleted lists with bullets and font
      information taken from the template file.

Slide specific content like this:


which is updated when the template changes.

An Inkscape file is used as a template file to define the background, title position and font, fonts and positions for text at different levels of indentation, groups to be cloned and used as bullets, etc.

Content specific to a particular slide can also be created in Inkscape, this content merged with the template and text input to make the final slide, so changes to the template after a particular slide is edited in Inkscape are included.

Download

Copy the text below into a file called 'inkslide.py'. (How do you attach a non-image file on this wiki?)

Here is an example template. Use Save Link As to stop your browser displaying the file instead of downloading it.

How it works

First InkSlide parses the text input, something like the example at the top of this page. It looks for flowed text boxes in the template file with special @id attributes like fr_title for the title, and fr_tab2 for the second level of bulleted lists. Copies of these elements are made and placed in a layer called 'Texts'. At this point, the text is all piled up at the top of the page.

InkSlide saves the file, and uses Inkscape's command line --query-height and friends to find out how high the wrapped text pieces are. At it then repositions them properly, adds clones of the bullet markers, etc. Bullet markers have special @id attributes like bu_1 for the first level of bulleted list.

Finally, InkSlide checks to see if there's an id tag for the slide, and if so if there's a corresponding Inkscape SVG file with content for this particular slide. This content has to occur in layers whose name starts with Instance, and these layers have to be present in the template file, but you can have more than one, so they can be above and below the regular content. If such content is found, it's copied into the output slide.

The Template File

The template file is a regular Inkscape file. If this list seems hard, just use the example template and modify it to suit your needs.

Required contents:

  • A group or layer called gr_text, into which text will be placed.
  • A flowed text frame with an @id of fr_title to define the font and position for the slide title. This should contain text like Title goes here Sly because Sly includes letters which ascend and descend from the baseline to the greatest extent. The placeholder text must be only one line because it's used to calculate line height.
  • A group or layer called gr_title into which the title will be placed. This makes it easier to do special effects on the title like shadows.
  • Flowed text frames with an @id of fr_tabN for each N in 0, 1, 2, 3. fr_tab0 is used for top level non bulleted text. These should also include Sly or similar. The placeholder text must be only one line because it's used to calculate line height.
  • Objects or groups withan @id of bu_N for each N in 1, 2, 3, for the three levels of bulleted list.

The fr_* elements define the font, font-size, color etc. for the different levels of text. They also define the position and wrap width. See the example template file for a possible arrangement.

"A group or layer called 'X'" indicates either a group with an @id of 'X' or a layer with a label (name) of 'X'. When both exist the group will take precedence.

Optional contents:

  • A background, which should be placed in a group or layer called `gr_bg`
  • A foreground, which should be placed in a group or layer called `gr_fg`
  • Affiliation text and logos you want to appear on every slide, this could go in the background, or another named group or layer.
  • A blurred clone of the gr_title element, offset and behind it. It's easier to do this is `gr_title` is a group rather than a layer.
  • Layers whose name starts with Instance - these are the layers the user can edit directly in Inkscape for a particular slide.

When you save the template file you should make sure that layers containing place holder text (the fr_* elements) and bullets are invisible. Likewise the Texts layer should be visible, as well as any Instance layers.

Text Input

InkSlide uses reStructuredText for markup, although it renders only a subset of rst.

Inkslide: an example
++++++++++++++++++++

This text will appear as a top level paragraph.

- Dash prefix indicates this text will appear as a bulleted item.

  - Indented dash prefix -> second level bulleted list.

.. is:pause

Two slides will be produced for this input, one with everything up to the
".. is:pause", and one with that plus the remaining content.
Incremental list display can be done this way.

The is:id tag below indicates that a file with a name like
'extra/is_intro.svg' should be inspected for extra content
for this particular slide.

.. is:id is_intro

Inkslide: the next slide...
+++++++++++++++++++++++++++

The GUI

A GUI for InkSlide is available as a plug-in for the Leo outline editor. This makes slide re-arranging easy, and adds some macro substitution capability. It also adds buttons for editing slide specific content in Inkscape.

FIXME: post plug-in on Leo wiki and link.

To do / status

InkSlide is very usable, but is missing some features. Remember you can use the slide specific editing feature to do any of these things and anything else you can do in Inkscape already, these will just be more convenient in the text input when they're done.

  • Inline text formatting (bold, color, etc.) for part of a piece of text
  • Justification switching
  • Verbatim code display
  • Simple image inclusion
  • more helper tools to make PNG and PDF versions of slides
  • configuring / using a directory for edited slides
  • command line flags for generating specific slides

Currently InkSlide is slow, because it must invoke Inkscape once for every single dimension it needs to retrieve. I'll try and get a --query-all switch into 0.46.

Bugs

  • There may be an issue with the way slide specific content is merged into the template producing conflicting @id attributes, but I haven't seen this happen yet.

Author / Credits

InkSlide was written by Terry Brown, terry-n-brown@yahoo.com - but use underscore, not '-'.

InkSlide was inspired by MagicPoint

Matt Harrison suggested using reStructured text as the markup language.

Code

"""
"""

# inkslide.py $Id$
# Author: Terry Brown
# Created: Thu Oct 11 2007

import lxml.etree as ET

import copy
import subprocess
import os.path
from docutils.core import publish_string

import sys  # for debug
class inkSlide(object):
    def __init__(self):

        self.NS = { 'i': 'http://www.inkscape.org/namespaces/inkscape',
           's': 'http://www.w3.org/2000/svg',
           'xlink' : 'http://www.w3.org/1999/xlink'}

        # number of defined tablevels, FIXME, could derive from template?
        self.tabLevels = 4  

        self._nextId = 0

        self.gap = {
            'bIMAGE': 15,   # gap before image
            'aIMAGE': 15,   # gap after image
            'aTAB0': 10,    # gap after top level text
            'aTAB1': 10,    # gap after level 1 bullet
            'aTAB2': 10,    # gap after level 2 bullet
            'aTAB3': 10,    # gap after level 3 bullet
            'fVCENTER': 1,  # flag - center vertically
            'dHEIGHT': 728, # dimension, bottom of page
            'fHCENTER': 1,  # flag - center vertically
            'dWIDTH': 1024, # dimension, bottom of page
        }

        self.dimCache = {}
        self.is_reads, self.is_cache = 0, 0
    def __del__(self):
        print '%d reads, %d cache hits' % (self.is_reads, self.is_cache)
    def nextId(self):
        """return an unsed Id"""
        # FIXME, should inspect template doc.
        self._nextId += 1
        return 'is'+str(self._nextId)

    def clearId(self, x, what='id', NS={}):
        """recursively clear @id on element x and descendants"""
        if not NS: NS = self.NS
        if what in x.keys():
            del x.attrib[what]
        for i in x.xpath('.//*[@%s]'%what, NS):
            del i.attrib[what]
        return x
    def getDim(self, fn, Id, what):
        """return dimension of element in fn with @id Id, what is
        x, y, width, height
        """

        hsh = fn+Id+what
        if hsh in self.dimCache:
            self.is_cache += 1
            return self.dimCache[hsh]

        cmd = ('inkscape', '--without-gui', '--query-id', Id, '--query-'+what, fn)

        proc = subprocess.Popen(cmd, stdout = subprocess.PIPE,
                                stderr = subprocess.PIPE)
        # make new pipe for stderr to supress chatter from inkscape
        proc.wait()
        self.dimCache[hsh] = proc.stdout.read()
        if self.dimCache[hsh].strip() == '':
            print "Warning: '%s' not found" % Id
        self.is_reads += 1
        return self.dimCache[hsh]
    def textReader(self, text):
        """NOT USED iterate parts in input text"""
        if not text: return
        for line in text.split('\n'):
            if line.startswith('%slide '):
                yield {'type':'TITLE', 's':line[7:].strip()}
                continue
            if line.startswith('%pause'):
                yield {'type':'PAUSE'}
                continue
            if line.startswith('%id '):
                yield {'type':'ID', 's':line[4:].strip()}
                continue
            if not line.strip():
                yield {'type':'BLANK'}
                continue
            tabs = len(line) - len(line.lstrip('\t'))
            yield {'type':'TAB', 'tabs':tabs, 's':line.strip()}

    def textReaderRst(self, text):

        xml = publish_string(text.replace('\t','    '), writer_name = 'xml')
        doc = ET.fromstring(xml)
        # yield {'type':'TITLE', 's':doc.xpath('/document/@title')[0]}
        nodes = 'paragraph', 'comment', 'title', 'image'
        for i in doc.xpath('//%s' % '|//'.join(nodes)):

            if i.tag == 'title':
                yield {'type':'TITLE', 's':i.text}
            if i.tag == 'paragraph':
                tabs = len(i.xpath('ancestor::list_item'))
                yield {'type':'TAB', 'tabs':tabs, 's':i.text, 'e':i}
            if i.tag == 'image':
                tabs = len(i.xpath('ancestor::list_item'))
                yield {'type':'IMAGE', 'uri':i.get('uri')}
            if i.tag == 'comment':

                comparts = i.text.split(None, 1)

                if len(comparts) > 1:
                    ctag, cval = comparts
                elif len(comparts) == 1:
                    ctag = comparts[0]
                else:
                    continue  # empty comment had no meaning to InkView

                if ctag == 'is:id':
                    try:
                        yield {'type':'ID', 's':cval}
                    except:
                        raise Exception('"id" comment requires a value')
                elif ctag == 'is:notitle':
                    yield {'type':'DROP', 'what':('gr_title',)}
                elif ctag == 'is:notext':
                    yield {'type':'DROP', 'what':('gr_text',)}
                elif ctag == 'is:nobackground':
                    yield {'type':'DROP', 'what':('gr_bg',)}
                elif ctag == 'is:noforeground':
                    yield {'type':'DROP', 'what':('gr_fg',)}
                elif ctag == 'is:blank':
                    yield {'type':'DROP', 'what':('gr_title','gr_text')}
                elif ctag == 'is:empty':
                    yield {'type':'DROP', 'what':('gr_bg','gr_title','gr_text','gr_fg')}
                elif ctag == 'is:no':
                    try:
                        yield {'type':'DROP', 'what':(cval,)}
                    except:
                        raise Exception('"no" comment requires a value')

                if ctag == 'is:dim':
                    dim, val = cval.strip().split()
                    yield {'type':'DIM', 'dim':dim, 'val':val}
    def groupOrLayer(self, g):
        dst = self.doc.xpath('//s:g[@id="%s"]'%g, self.NS)
        if not dst: dst = self.doc.xpath('//s:g[@i:label="%s"]'%g, self.NS)
        if not dst: return dst  # let the caller test, don't raise exception here
        dst = dst[0]
        return dst
    def instanceFromTemplate(self, template, instance, text, pauseOk = True):

        slide = 0

        hlist = []
        self.doc = ET.parse(template)
        slideId = None

        for t in self.textReaderRst(text):

            if t['type'] == 'ID':
                slideId = t['s']
                continue

            if t['type'] == 'TITLE':
                if hlist:
                    self.process(instance, hlist, slideId, slide)
                    slide = slide + 1
                    hlist = [t]
                    self.doc = ET.parse(template)
                    slideId = None
                else:
                    hlist.append(t)

                print t['s']

                dst = self.groupOrLayer('gr_title')
                src = self.doc.xpath('//s:flowRoot[@id="fr_title"]', self.NS)[0]
                cpy = self.clearId(copy.deepcopy(src))
                Id = self.nextId()
                cpy.set('id', Id)
                hlist[-1]['id']=Id
                cpy.xpath('s:flowPara',self.NS)[0].text = t['s']
                dst.append(cpy)
                continue

            if t['type'] == 'PAUSE':
                if pauseOk:
                    self.process(instance, hlist, slideId, slide)
                    slide = slide + 1
                continue

            if t['type'] == 'BLANK':
                hlist.append(t)
                continue

            if t['type'] == 'TAB':
                hlist.append(t)
                dst = self.groupOrLayer('gr_text')
                src = self.doc.xpath('//s:flowRoot[@id="fr_tab%d"]' % t['tabs'], self.NS)[0]
                cpy = self.clearId(copy.deepcopy(src))
                Id = self.nextId()
                cpy.set('id', Id)
                hlist[-1]['id']=Id
                # cpy.xpath('s:flowPara',self.NS)[0].text = t['s']
                self.flowParaFromElement(
                    cpy.xpath('s:flowPara',self.NS)[0], t['e'])
                dst.append(cpy)
                continue

            if t['type'] == 'IMAGE':
                hlist.append(t)
                dst = self.groupOrLayer('gr_image')
                if not dst:
                    dst = self.groupOrLayer('gr_text')
                img = ET.Element('image')
                img.set('{%s}href'%self.NS['xlink'], t['uri'])
                Id = self.nextId()
                img.set('id', Id)
                hlist[-1]['id']=Id
                dst.append(img)
                continue

            hlist.append(t)  # whatever it was

        self.process(instance, hlist, slideId, slide)
    def flowParaFromElement(self, fp, e):
        fp.text = e.xpath('string(.)')
    def process(self, instance, hlist, slideId, slide):

        instanceName = instance.replace('%s', '%04d'%slide)

        instance = 'tmp.svg'  # reusing allows caching in getDim, but relies on nextId ensuring
                              # that the changing parts have new ids

        self.doc.write(file(instance, 'w'))

        drop = []

        bullDim = {}
        tabDim = {}
        for n in range(0,self.tabLevels):
            b = {}
            x = self.doc.xpath('//*[@id="bu_%d"]'%n, self.NS)
            if x:
                # self.clearId(x[0], what='transform')
                for i in 'x', 'y', 'width', 'height':
                    b[i] = float(self.getDim(instance, 'bu_%d'%n, i))
                bullDim[n] = b
            b = {}
            x = self.doc.xpath('//*[@id="fr_tab%d"]'%n, self.NS)
            if x:
                # clearId(x[0], what='transform')
                for i in 'x', 'y', 'width', 'height':
                    b[i] = float(self.getDim(instance, 'fr_tab%d'%n, i))
                x = x[0].xpath('.//s:rect',self.NS)[0]
                b['max-width'] = float(x.get('width'))
                b['max-height'] = float(x.get('height'))
                tabDim[n] = b

        delta = 0.
        firstVert = True
        placed_xy = []
        placed_transform = []

        for n, i in enumerate(hlist):

            this = hlist[n]
            prev = hlist[n-1]

            if this['type'] == 'DROP': drop += this['what']

            if this['type'] == 'DIM':
                if this['dim'] not in self.gap or this['val'][0] not in '+-':
                    self.gap[this['dim']] = float(this['val'])
                else:
                    self.gap[this['dim']] += float(this['val'])

            zeroHeight = ['TITLE', 'DROP', 'DIM']

            # don't do this - need to add height from prev to delta
            #if this['type'] in zeroHeight:
            #    continue  # already in right place, not a vertical element

            hgt = 0
            if prev['type'] == 'BLANK':
                hgt = 30
            elif prev['type'] not in zeroHeight:
                hgt = float(self.getDim(instance, prev['id'], 'height'))

                # risky gambit to space bullets properly when some lines
                # have no ascenders etc.
                if prev['type'] == 'TAB' and prev['tabs'] > 0:
                    lnh = tabDim[prev['tabs']]['height']
                    hgt = lnh * int(hgt/lnh+0.5)

            # TAB0, TAB1, TITLE, IMAGE, etc.
            # gap after previous
            hsh = 'a' + prev['type'] + str(prev.get('tabs',''))
            hgt += self.gap.get(hsh, 0)
            # gap before current
            hsh = 'b' + this['type'] + str(this.get('tabs',''))
            hgt += self.gap.get(hsh, 0)

            if firstVert:
                hgt = 0
                firstVert = False

            delta += hgt

            if this['type'] == 'BLANK': continue

            if this['type'] == 'TAB':
                tabs = this['tabs']

                r = self.doc.xpath('//s:flowRoot[@id="%s"]' % this['id'], self.NS)[0]
                self.clearId(r, what='transform')
                r = r.xpath('.//s:rect', self.NS)[0]
                r.set('x', str(tabDim[tabs]['x']))
                y = tabDim[tabs]['y'] + delta
                #print '%s -> %s' % (r.get('y'), y)
                #print tabDim[tabs]['y']
                r.set('y', str(y))
                placed_xy.append([r, this['id']])

            if (this['type'] == 'TAB' and this['tabs'] in bullDim
                and 'isDone' not in this):
                this['isDone'] = True
                dx = (tabDim[tabs]['x'] 
                      - bullDim[tabs]['x']
                      - 1.5*bullDim[tabs]['width']
                      )
                dy = (y 
                      + tabDim[tabs]['height'] / 2.
                      - bullDim[tabs]['y'] 
                      - bullDim[tabs]['height'] / 2.
                      )
                # dst = doc0.xpath('//s:g[@i:label="Texts"]', NS)[0]
                dst = self.groupOrLayer('gr_text')
                #print y, tabDim[tabs]['height'], bullDim[tabs]['y'], bullDim[tabs]['height'], dy
                clone = ET.Element('use')
                clone.set('transform', 'translate(%f,%f)' % (dx,dy))
                clone.set('{%s}href'%self.NS['xlink'], '#bu_%d'%tabs)
                placed_transform.append([clone, dx, dy])
                dst.append(clone)

            if this['type'] == 'IMAGE':

                xpath = '//s:image[@id="%s"]' % this['id']  # doesn't work?
                xpath = '//*[@id="%s"]' % this['id']
                i = self.doc.xpath(xpath, self.NS)[0]
                w = float(self.getDim(instance, this['id'], 'width'))
                x = tabDim[0]['x']+tabDim[0]['max-width']/2-w/2
                i.set('x', str(x))
                y = tabDim[0]['y'] + delta
                i.set('y', str(y))
                placed_xy.append([i,this['id']])

        if self.gap['fVCENTER']:

            if 'id' in this:
                delta += float(self.getDim(instance, this['id'], 'height'))

            recenter = (self.gap['dHEIGHT'] - delta - tabDim[0]['y']) * 0.45

            for i in placed_transform:
                i[0].set('transform', 'translate(%f,%f)'%(i[1],i[2]+recenter))
                i[2] += recenter  # so it's not lost below
            for i in placed_xy:
                i[0].set('y', str(float(i[0].get('y'))+recenter))

        if self.gap['fHCENTER']:

            maxX = 0
            for i in placed_xy:
                x = float(self.getDim(instance, i[1], 'x'))
                x += float(self.getDim(instance, i[1], 'width'))
                if x > maxX: maxX = x

            recenter = (self.gap['dWIDTH'] - maxX - tabDim[0]['x']) * 0.5

            for i in placed_transform:
                i[0].set('transform', 'translate(%f,%f)'%(i[1]+recenter,i[2]))
                i[1] += recenter  # in case it's used again
            for i in placed_xy:
                if i[0].tag != 'image':  # images already centered
                    # print i[0].tag
                    i[0].set('x', str(float(i[0].get('x'))+recenter))

        # look for instance specific parts
        if slideId:
            f = self.instancePath(slideId)
            if os.path.isfile(f):
                comp = ET.parse(f)
                svg = self.doc.xpath('//s:svg', self.NS)[0]
                g = "{%s}g"%self.NS['s']
                k = "{%s}label"%self.NS['i']
                for n, i in enumerate(svg.getchildren()):
                    if i.tag == g and i.get(k, '').startswith('Instance'):
                        x = comp.xpath('//s:svg//s:g[@i:label="%s"]'%i.get(k), self.NS)
                        if x:
                            x = x[0]
                            cpy = self.clearId(copy.deepcopy(x))
                            # cpy = self.textCopy(x)
                            svg[n] = cpy
                defs = comp.xpath('//s:svg/s:defs/*', self.NS)
                dst = self.doc.xpath('//s:svg/s:defs', self.NS)[0]
                for d in defs:
                    Id = d.get('id')
                    current = self.doc.xpath('//s:svg/s:defs/*[@id="%s"]'%Id, self.NS)
                    if not current:
                        cpy = copy.deepcopy(d)
                        dst.append(cpy)

        for i in drop:
            e = self.groupOrLayer(i)
            if e:
                e.xpath('..')[0].remove(e)

        # problem losing xlink ns?
        for i in self.doc.xpath('//*[@href]'):
            i.set('{%s}href'%self.NS['xlink'], i.get('href'))

        self.doc.write(file(instanceName, 'w'))

        return slideId
    def textCopy(self, ele):
        """copy the element ele via xml->text->xml, as deepcopy seems to lose text"""
        txt = ET.tostring(ele)
        # print txt
        return ET.fromstring(txt)
    def instancePath(self, slideId):
        """return path to component for slide"""
        # return os.path.join(slideId, component)
        return slideId+'.svg'
    def writeComponents(self, inkscapeFile, slideId):
        """write components from file for slide"""
        if not slideId: return
        doc0 = ET.parse(inkscapeFile)
        hasComponents = False
        for i in doc0.xpath('//s:svg//s:g', self.NS):
            k = "{%s}label"%self.NS['i']
            # print i.keys(),k
            if (k in i.keys() and i.get(k).startswith('Instance')):
                if len(i) > 0:
                    hasComponents = True
                    break
                    # ET.ElementTree(i).write(f)

        f = self.instancePath(slideId)

        if hasComponents:
            if (os.path.abspath(os.path.realpath(inkscapeFile)) != 
                os.path.abspath(os.path.realpath(f))):
                file(f, 'w').write(file(inkscapeFile).read())
        else:
            if os.path.isfile(f):
                os.remove(f)

if __name__ == '__main__':
    import sys
    x = inkSlide()
    x.instanceFromTemplate(sys.argv[1], 'slide%s.svg', file(sys.argv[2]).read())