forked from evil-mad/EggBot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy patheggbot_spiraltext.py
executable file
·449 lines (385 loc) · 17.3 KB
/
eggbot_spiraltext.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# 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()