# eggbot_spiraltext.py
#
# Render a passage of text using the Hershey fonts, then stretch it so
# that it will wrap multiple times around an egg, and finally tilt it
# so that it will spiral as it wraps.
#
# + The wrapping need not be an integral multiple of 3200 pixels
#
# + The text tilt is computed to use the full height of the document
#
# + The text can be run starting from the top of the page or from
# the bottom (and upside down). This latter orientation is useful
# when placing the bottom of the egg (fat end) in the egg motor's
# egg cup
#
# + The text can be stretched more horizontally than vertically to
# compensate for some of the geometry issues associated with drawing
# on eggs.
#
# + The text can contain markup (see below)
#
# This extension also permits some basic markup of the passage using
# XHTML-like conventions and a limited set of tags:
#
# <sams> - A simple typeface which lacks serifs
# <times> - "Times" like typeface (a face with serifs)
# <script> - A flowing script font
# <b> - Boldface
# <em> - Emphasis
# <i> - Italics
# <face> - Where "face" is any of the typeface names from hersheydata.py
#
# The markup processing is not XML-conformant: we don't expect a well-formed
# document as input. No single root element is required. And, at the end of
# the text, we do not require closure of any open tags. We do however enforce
# proper nesting of tags: an element cannot be closed unless its children have
# already been closed. This is more to prevent ambiguity about whether or
# not closing a typeface also closes any markup operating under it (e.g.,
# does <sans><b>text</sans> mean that the <b> was implicitly ended when
# </sans> was encountered?
#
# This extension requires the hersheydata.py file which is part of the
# Hershey Text rendering extension written by Windell H. Oskay of
# www.evilmadscientist.com. Information on that extension may be found at
#
# http://www.evilmadscientist.com/go/hershey
#
# Copyright 2011, Daniel C. Newman,
#
# Significant portions of this code were written by Windell H. Oskay and are
# Copyright 2011, Windell H. Oskay, www.evilmadscientist.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import sys
import hersheydata #data file w/ Hershey font data
import inkex
import simplestyle
import math
# Mapping table to map the names used here to the corresponding
# names used in hersheydata.py. This helps prevent end users from
# being impacted by a name change in hersheydata.py. This can also
# be used to deal with a face being removed from hersheydata.py
map_our_names_to_hersheydata = {
'astrology' : 'astrology',
'cursive' : 'cursive',
'cyrillic' : 'cyrillic',
'futural' : 'futural',
'futuram' : 'futuram',
'gothiceng' : 'gothiceng',
'gothicger' : 'gothicger',
'gothicita' : 'gothicita',
'greek' : 'greek',
'japanese' : 'japanese',
'markers' : 'markers',
'mathlow' : 'mathlow',
'mathupp' : 'mathupp',
'meteorology' : 'meteorology',
'music' : 'music',
'scriptc' : 'scriptc',
'scripts' : 'scripts',
'symbolic' : 'symbolic',
'timesg' : 'timesg',
'timesi' : 'timesi',
'timesib' : 'timesib',
'timesr' : 'timesr',
'timesrb' : 'timesrb' }
# The following two routines are lifted with impunity from Windell H. Oskay's
# hershey.py Hershey Text extension for Inkscape. They are,
# Copyright 2011, Windell H. Oskay, www.evilmadscientist.com
def draw_svg_text(char, face, offset, vertoffset, parent):
style = { 'stroke': '#000000', 'fill': 'none' }
pathString = face[char]
splitString = pathString.split()
midpoint = offset - int(splitString[0])
i = pathString.find("M")
if i >= 0:
pathString = pathString[i:] #portion after first move
trans = 'translate(' + str(midpoint) + ',' + str(vertoffset) + ')'
text_attribs = {'style':simplestyle.formatStyle(style), 'd':pathString, 'transform':trans}
inkex.etree.SubElement(parent, inkex.addNS('path','svg'), text_attribs)
return midpoint + int(splitString[1]) #new offset value
def renderText( parent, markup ):
# Embed text in group to make manipulation easier:
g_attribs = {inkex.addNS('label','inkscape'):'Hershey Text' }
g = inkex.etree.SubElement(parent, 'g', g_attribs)
w = 0 #Initial spacing offset
spacing = 3 # spacing between letters
for Face, Text in markup:
if map_our_names_to_hersheydata.has_key(Face):
Face = map_our_names_to_hersheydata[Face]
font = eval('hersheydata.' + Face)
letterVals = [ord(q) - 32 for q in Text]
for q in letterVals:
if (q < 0) or (q > 95):
w += 2*spacing
else:
w = draw_svg_text(q, font, w, 0, g)
return g, w
# The generic font "families" we support
generic_families = ( 'sans', 'script', 'times' )
# Convert "family-name" + "bold-0-or-1" + "italic-0-or-1" to a typeface name
family_to_font = {
'sans00' : 'futural', 'sans10' : 'futuram', 'sans01' : 'futural', 'sans11' : 'futuram',
'times00' : 'timesr', 'times10' : 'timesrb', 'times01' : 'timesi', 'times11' : 'timesib',
'script00' : 'scripts', 'script10' : 'scriptc', 'script01' : 'scripts', 'script11' : 'scriptc' }
emphasis_is_bold = { 'sans' : True, 'times' : False, 'script' : True }
# Short list of entity references
entity_refs = { '<' : '<', '>' : '>', '&' : '&', '"' : '"', '&apos' : "'", ' ' : ' ' }
def pickFace( family, bold=False, italics=False, emphasis=False ):
if ( family is None ) or ( family == '' ):
return None
b = '0'
i = '0'
# If using a generic font family, then determine how to map <em>
if emphasis and ( family in generic_families ):
if emphasis_is_bold[family]:
bold = True
else:
italics = True
if bold:
b = '1'
if italics:
i = '1'
if family_to_font.has_key( family + b + i ):
return family_to_font[family + b + i]
return family
def processMarkup( text, family='sans' ):
if ( text is None ):
text = ''
# By default we assume 'sans'
if ( family is None ) or ( family == ''):
family = 'sans'
family_default = family
face_stack = [ family ]
# Bold and italics off
bold = False
emphasis = False
italic = False
# Set the current typeface
face = pickFace( family, bold, italic, emphasis )
# And the result of markup processing so far
markup = []
# We keep a queue / list of the open markup tags
# When a tag is closed, we expect it to be well nested. To enforce
# that expectation, we make sure that we are closing the most recently
# opened tag. While this may seem overly picky, it's easier than worrying
# issues like, "Does closing a typeface imply implicitly closing <b> or <it>?"
# And, "Does starting a new typeface imply closing the prior selected face?"
tags_used = []
outstr = ''
i = 0
while i < len( text ):
# An entity reference?
if text[i] == '&':
j = text.find( ';', i+1 )
if ( j != -1 ):
eref = text[i:j+1]
if entity_refs.has_key[eref]:
outstr += entity_refs[eref]
i = j + 1
else:
inkex.errormsg( 'Ignoring the unrecognized entity reference %s.' % eref )
outstr += eref
i = j + 1
else:
inkex.errormsg( 'An unescaped "&" was encountered; please replace it with "&".' )
break
# Start of a tag (start-tag or end-tag? self-closing tags not supported)
elif text[i] == '<':
j = text.find( '>', i+1 )
if ( j != -1 ) and ( j > ( i + 1) ):
tag = text[i+1:j]
i = j + 1
if tag[0] == '/':
# This is an end-tag (closing tag)
close = True
tag = tag[1:]
# Ensure that the most recently opened tag is that which we are closing here
# We'll pop the most recent tag from the queue of opened tags and see if
# it matches
if len( tags_used ) == 0:
inkex.errormsg( 'The ending tag </%s> appeared before any start tag <%s>.' % ( tag, tag ) )
break
else:
old_tag = tags_used.pop()
if old_tag != tag:
inkex.errormsg( 'The ending tag </%s> does not appear to be correctly nested; it tried to close the tag <%s>. Sorry, but all tags must be properly nested.' % ( tag, old_tag ) )
break
else:
# Start tag (opening tag)
# Push it onto the queue of opened tags
close = False
tags_used.append( tag )
if ( tag == 'b' ) or ( tag == 'strong' ):
if bold == close:
# Push prior string and font onto the stack
if outstr != '':
markup.append( [ face, outstr ] )
outstr = ''
# Start a new boldface string
bold = not bold
face = pickFace( family, bold, italic, emphasis )
elif tag == 'i':
if italic == close:
# Push the prior string and font unto the stack
if outstr != '':
markup.append( [ face, outstr ] )
outstr = ''
# Start a new italicized string
italic = not italic
face = pickFace( family, bold, italic, emphasis )
elif tag == 'em':
if emphasis == close:
# Push the prior string and font unto the stack
if outstr != '':
markup.append( [ face, outstr ] )
outstr = ''
# Start a new italicized string
emphasis = not emphasis
face = pickFace( family, bold, italic, emphasis )
else:
if ( tag not in generic_families ) and \
( not map_our_names_to_hersheydata.has_key( tag ) ):
if close:
inkex.errormsg( 'Ignoring the unrecognized tag </%s>.' % tag )
else:
inkex.errormsg( 'Ignoring the unrecognized tag <%s>.' % tag )
else:
if outstr != '':
markup.append( [face, outstr] )
outstr = ''
if not close:
family = tag
face_stack.append( family )
else:
if len( face_stack ) > 0:
# Current face on the stack should be the one we just closed
face_stack.pop()
if len( face_stack ) > 0:
family = face_stack[len( face_stack) - 1]
else:
family = default_family
else:
family = default_family
face = pickFace( family, bold, italic, emphasis )
else:
inkex.errormsg( 'Ignoring unescaped "<"' )
outstr += '<'
i += 1
else:
outstr += text[i]
i += 1
# We won't worry about unclosed tags -- we're not trying to be an XML or XHTML parser
# See if there was a hard error
if i < len( text):
return None
# And push the last text into the list of processed markup
if outstr != '':
markup.append( [face, outstr] )
return markup
class SpiralText( inkex.Effect ):
def __init__( self ):
inkex.Effect.__init__( self )
self.OptionParser.add_option( "--tab", #NOTE: value is not used.
action="store", type="string",
dest="tab", default="splash",
help="The active tab when Apply was pressed" )
self.OptionParser.add_option( "--text",
action="store", type="string",
dest="text", default="Hershey Text for Inkscape",
help="The input text to render")
self.OptionParser.add_option( "--fontfamily",
action="store", type="string",
dest="fontfamily", default="sans",
help="The selected font face when Apply was pressed" )
self.OptionParser.add_option( "--wrap",
action="store", type="float",
dest="wrap", default=float(10),
help="Number of times to wrap the text around the egg" )
self.OptionParser.add_option( "--flip",
action="store", type="inkbool",
dest="flip", default=False,
help="Flip the text for plotting with the egg's bottom at the egg motor" )
self.OptionParser.add_option( "--stretch",
action="store", type="inkbool",
dest="stretch", default=True,
help="Stretch the text horizontally to account for egg distortions" )
def effect( self ):
markup = processMarkup( self.options.text, self.options.fontfamily )
g,w = renderText( self.current_layer, markup )
# Now to wrap the text N times around the egg, we need to scale it to have
# length 3200 * N. It's current width is w so the scale factor is (3200 * N) / w.
scale_x = float( 3200 * self.options.wrap ) / float( w )
scale_y = scale_x
if self.options.stretch:
scale_y = scale_y * 2.0 / 3.0
# In planning the scaling, we'd like to know the height of our line of text.
# Rather than computing its bounding box, we'll just use the height of the
# parens from the Simplex Roman font. And, we could compute that but we'll
# just use our prior knowledge of it being 32.
h = float( 32.0 )
# And the angular tilt will be arcsine( height / (3200 * fWrap) )
svg = self.document.getroot()
height = float( self.unittouu( svg.attrib['height'] ) ) - h * scale_y
angle = ( float( 180 ) / math.pi ) * \
math.asin( height / float( 3200 * self.options.wrap ) )
if self.options.flip:
angle += float( 180.0 )
t = 'translate(%f,%f) rotate(%f,%f,0) scale(%f,%f)' % ( -w*scale_x, h*scale_y, angle,
w*scale_x, scale_x, scale_y )
else:
t = 'translate(0,%f) rotate(%f,0,0) scale(%f,%f)' % ( h, angle, scale_x, scale_y )
g.set( 'transform', t)
if __name__ == '__main__':
e = SpiralText()
e.affect()