···
Additional keyword arguments are output as additional arguments to the
381
-
SVG node e.g. fill="red", font_size=20, text_anchor="middle". '''
381
+
SVG node e.g. fill='red', font_size=20, text_anchor='middle',
382
+
letter_spacing=1.5.
384
+
CairoSVG bug with letter spacing text on a path: The first two letters
385
+
are always spaced as if letter_spacing=1. '''
384
-
def __init__(self, text, fontSize, x, y, center=False, valign=None,
385
-
lineHeight=1, **kwargs):
386
-
singleLine = isinstance(text, str)
388
-
text = text.splitlines()
392
-
numLines = len(text)
388
+
def __new__(cls, text, *args, path=None, id=None, _skipCheck=False,
390
+
# Check for the special case of multi-line text on a path
391
+
# This is inconsistently implemented by renderers so we return a group
392
+
# of single-line text on paths instead.
393
+
if path is not None and not _skipCheck:
394
+
text, _ = cls._handleTextArgument(text, True)
398
+
for i, line in enumerate(text):
399
+
subtext = [None] * len(text)
401
+
g.append(Text(subtext, *args, path=path, _skipCheck=True,
404
+
return super().__new__(cls)
405
+
def __init__(self, text, fontSize, x=None, y=None, *, center=False,
406
+
valign=None, lineHeight=1, lineOffset=0, path=None,
407
+
startOffset=None, pathArgs=None, tspanArgs=None,
408
+
cairoFix=True, _skipCheck=False, **kwargs):
409
+
# Check argument requirements
411
+
if x is None or y is None:
413
+
"__init__() missing required arguments: 'x' and 'y' "
414
+
"are required unless 'path' is specified")
398
-
if 'text_anchor' not in kwargs:
399
-
kwargs['text_anchor'] = 'middle'
402
-
# Backwards compatible centering
403
-
centerOffset = fontSize*0.5*center
405
-
emOffset = 0.4 - lineHeight * (numLines - 1) / 2
420
+
if x is not None or y is not None:
422
+
"__init__() conflicting arguments: 'x' and 'y' "
423
+
"should not be used when 'path' is specified")
424
+
if pathArgs is None:
426
+
if startOffset is not None:
427
+
pathArgs.setdefault('startOffset', startOffset)
428
+
if tspanArgs is None:
430
+
onPath = path is not None
432
+
text, singleLine = self._handleTextArgument(text, forceMulti=onPath)
433
+
numLines = len(text)
436
+
centerCompat = False
437
+
if center and valign is None:
439
+
centerCompat = singleLine and not onPath
440
+
if center and kwargs.get('text_anchor') is None:
441
+
kwargs['text_anchor'] = 'middle'
407
-
emOffset = 0.4 - lineHeight * (numLines - 1) / 2
443
+
if centerCompat: # Backwards compatible centering
444
+
lineOffset += 0.5 * center
446
+
lineOffset += 0.4 - lineHeight * (numLines - 1) / 2
411
-
emOffset = -lineHeight * (numLines - 1)
414
-
fontSize = float(fontSize)
418
-
translate = 'translate(0,{})'.format(centerOffset)
419
-
if 'transform' in kwargs:
420
-
kwargs['transform'] += ' ' + translate
422
-
kwargs['transform'] = translate
423
-
super().__init__(x=x, y=-y, font_size=fontSize, **kwargs)
450
+
lineOffset += -lineHeight * (numLines - 1)
452
+
dy = '{}em'.format(lineOffset)
453
+
kwargs.setdefault('dy', dy)
454
+
# Text alignment on a path
456
+
if kwargs.get('text_anchor') == 'start':
457
+
pathArgs.setdefault('startOffset', '0')
458
+
elif kwargs.get('text_anchor') == 'middle':
459
+
pathArgs.setdefault('startOffset', '50%')
460
+
elif kwargs.get('text_anchor') == 'end':
461
+
if cairoFix and 'startOffset' not in pathArgs:
462
+
# Fix CairoSVG not drawing the last character with aligned
464
+
tspanArgs.setdefault('dx', -1)
465
+
pathArgs.setdefault('startOffset', '100%')
467
+
super().__init__(x=x, y=y, font_size=fontSize, **kwargs)
468
+
self._textPath = None
425
-
self.escapedText = xml.escape(text)
470
+
self.escapedText = xml.escape(text[0])
472
+
# Add elements for each line of text
428
-
# Text is an iterable
429
-
for i, line in enumerate(text):
430
-
dy = '{}em'.format(emOffset if i == 0 else lineHeight)
431
-
self.appendLine(line, x=x, dy=dy)
475
+
# Text is an iterable
476
+
for i, line in enumerate(text):
477
+
dy = '{}em'.format(lineOffset if i == 0 else lineHeight)
478
+
self.appendLine(line, x=x, dy=dy, **tspanArgs)
480
+
self._textPath = _TextPath(path, **pathArgs)
481
+
assert sum(bool(line) for line in text) <= 1, (
482
+
'Logic error, __new__ should handle multi-line paths')
483
+
for i, line in enumerate(text):
484
+
if not line: continue
485
+
dy = '{}em'.format(lineOffset + i*lineHeight)
486
+
tspan = TSpan(line, dy=dy, **tspanArgs)
487
+
self._textPath.append(tspan)
488
+
self.append(self._textPath)
490
+
def _handleTextArgument(text, forceMulti=False):
491
+
# Handle multi-line text (contains '\n' or is a list of strings)
492
+
singleLine = isinstance(text, str)
493
+
if isinstance(text, str):
494
+
singleLine = '\n' not in text and not forceMulti
498
+
text = tuple(text.splitlines())
503
+
return text, singleLine
def writeContent(self, idGen, isDuplicate, outputFile, dryRun):
···
child.writeSvgElement(idGen, isDuplicate, outputFile, dryRun)
def appendLine(self, line, **kwargs):
519
+
if self._textPath is not None:
520
+
raise ValueError('appendLine is not supported for text on a path')
self.append(TSpan(line, **kwargs))
523
+
class _TextPath(DrawingParentElement):
524
+
TAG_NAME = 'textPath'
526
+
def __init__(self, path, **kwargs):
527
+
super().__init__(xlink__href=path, **kwargs)
class _TextContainingElement(DrawingBasicElement):
''' A private parent class used for elements that only have plain text
···
outputFile.write(self.escapedText)
class TSpan(_TextContainingElement):
''' A line of text within the Text element. '''