kopia lustrzana https://github.com/evil-mad/EggBot
450 wiersze
17 KiB
Python
Executable File
450 wiersze
17 KiB
Python
Executable File
# coding=utf-8
|
|
# 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:
|
|
#
|
|
# <sans> - 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
|
|
#
|
|
# Small portions of this code were changed by Sheldon B. Michaels 2016,
|
|
# in order to accommodate the addition of several new faces
|
|
# (the "EMS" series) to hersheydata.py. Additionally, changes were made
|
|
# to the default text rendering style.
|
|
#
|
|
# 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 math
|
|
|
|
import hersheydata # data file w/ Hershey font data
|
|
import inkex
|
|
import simplestyle
|
|
|
|
# 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', 'stroke-linecap': 'round', 'stroke-linejoin': 'round'}
|
|
# Apply rounding to ends so that user gets best impression of final printed text appearance.
|
|
path_string = face[char]
|
|
split_string = path_string.split()
|
|
midpoint = offset - float(split_string[0])
|
|
i = path_string.find("M")
|
|
if i >= 0:
|
|
path_string = path_string[i:] # portion after first move
|
|
trans = 'translate(' + str(midpoint) + ',' + str(vertoffset) + ')'
|
|
text_attribs = {'style': simplestyle.formatStyle(style), 'd': path_string, 'transform': trans}
|
|
inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), text_attribs)
|
|
return midpoint + float(split_string[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 face in map_our_names_to_hersheydata:
|
|
face = map_our_names_to_hersheydata[face]
|
|
font = getattr(hersheydata, face)
|
|
letter_vals = (ord(q) - 32 for q in text)
|
|
for q in letter_vals:
|
|
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 normalize_possible_EMS_string(tag):
|
|
# Normalizes tag name by removing any spaces
|
|
s_normalized_tag = tag.replace(' ', '')
|
|
return s_normalized_tag
|
|
|
|
|
|
def is_valid_EMS_name(tag):
|
|
# returns true if family is one of the "EMS" faces in hersheydata.py
|
|
# else false
|
|
s_normalized_tag = normalize_possible_EMS_string(tag)
|
|
b_ret_val = False # default assumption
|
|
try:
|
|
fontgroup = hersheydata.group_allfonts
|
|
except:
|
|
# User probably has old version of hersheydata.py
|
|
pass
|
|
else:
|
|
for f in fontgroup:
|
|
if f[0] == s_normalized_tag:
|
|
b_ret_val = True
|
|
break
|
|
|
|
return b_ret_val
|
|
|
|
|
|
def pickFace(family, bold=False, italics=False, emphasis=False):
|
|
if not 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 + b + i) in family_to_font:
|
|
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 not family:
|
|
family = 'sans'
|
|
default_family = 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 eref in entity_refs:
|
|
outstr += entity_refs[eref]
|
|
i = j + 1
|
|
else:
|
|
inkex.errormsg('Ignoring the unrecognized entity reference {0}.'.format(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 </{0}> appeared before any start tag <{1}>.'.format(tag, tag))
|
|
break
|
|
else:
|
|
old_tag = tags_used.pop()
|
|
if old_tag != tag:
|
|
inkex.errormsg('The ending tag </{0}> does not appear to be correctly nested; it tried to close the tag <{1}>. Sorry, but all tags must be properly nested.'.format(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:
|
|
b_valid_ems_name = is_valid_EMS_name(tag)
|
|
if b_valid_ems_name:
|
|
tag = normalize_possible_EMS_string(tag)
|
|
if all([tag not in generic_families,
|
|
tag not in map_our_names_to_hersheydata,
|
|
not b_valid_ems_name]):
|
|
if close:
|
|
inkex.errormsg('Ignoring the unrecognized tag </{0}>.'.format(tag))
|
|
else:
|
|
inkex.errormsg('Ignoring the unrecognized tag <{0}>.'.format(tag))
|
|
else:
|
|
if outstr != '':
|
|
markup.append([face, outstr])
|
|
outstr = ''
|
|
if not close:
|
|
family = tag
|
|
face_stack.append(family)
|
|
else:
|
|
if face_stack:
|
|
# Current face on the stack should be the one we just closed
|
|
face_stack.pop()
|
|
if face_stack:
|
|
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=10.0,
|
|
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 = 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 = (180.0 / math.pi) * math.asin(height / float(3200 * self.options.wrap))
|
|
|
|
if self.options.flip:
|
|
angle += 180.0
|
|
t = 'translate({0:f},{1:f}) rotate({2:f},{3:f},0) scale({4:f},{5:f})'.format(-w * scale_x, h * scale_y, angle,
|
|
w * scale_x, scale_x, scale_y)
|
|
else:
|
|
t = 'translate(0,{0:f}) rotate({1:f},0,0) scale({2:f},{3:f})'.format(h, angle, scale_x, scale_y)
|
|
g.set('transform', t)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
e = SpiralText()
|
|
e.affect()
|