752 lines
32 KiB
Python
752 lines
32 KiB
Python
![]() |
#Copyright ReportLab Europe Ltd. 2000-2017
|
||
|
#see license.txt for license details
|
||
|
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/pdfgen/textobject.py
|
||
|
__version__='3.3.0'
|
||
|
__doc__="""
|
||
|
PDFTextObject is an efficient way to add text to a Canvas. Do not
|
||
|
instantiate directly, obtain one from the Canvas instead.
|
||
|
|
||
|
Progress Reports:
|
||
|
8.83, 2000-01-13, gmcm: created from pdfgen.py
|
||
|
"""
|
||
|
from reportlab.lib.colors import Color, CMYKColor, CMYKColorSep, toColor
|
||
|
from reportlab.lib.utils import isBytes, isStr, asUnicode
|
||
|
from reportlab.lib.rl_accel import fp_str
|
||
|
from reportlab.pdfbase.pdfmetrics import getFont as pdfmetrics_getFont, stringWidth as pdfmetrics_stringWidth, unicode2T1 as pdfmetrics_unicode2T1
|
||
|
from reportlab.pdfbase.ttfonts import ShapedStr, ShapeData, _sdGuardL, shapeStr
|
||
|
from itertools import groupby
|
||
|
from operator import itemgetter
|
||
|
|
||
|
#this is to handle the optionality of rlbidi
|
||
|
try:
|
||
|
import rlbidi
|
||
|
log2vis = rlbidi.log2vis
|
||
|
class BidiStr(str):
|
||
|
'''A str with indices visual __bidiV__, logical __bidiL__'''
|
||
|
def __new__(cls, s, bidiV=-1, bidiL=-1):
|
||
|
self = super().__new__(cls,s)
|
||
|
self.__bidiV__ = bidiV
|
||
|
self.__bidiL__ = bidiL
|
||
|
return self
|
||
|
isBidiStr = lambda _: isinstance(_,BidiStr)
|
||
|
class BidiList(list):
|
||
|
'''A list with indices visual __bidiV__, logical __bidiL__'''
|
||
|
def __init__(self, L, bidiV=-1, bidiL=-1):
|
||
|
super().__init__(L)
|
||
|
self.__bidiV__ = bidiV
|
||
|
self.__bidiL__ = bidiL
|
||
|
def __repr__(self):
|
||
|
return f'{self.__class__.__name__}({super().__repr__()},{self.__bidiV__},{self.__bidiL__})'
|
||
|
isBidiList = lambda _: isinstance(_,BidiList)
|
||
|
def bidiText(text,direction):
|
||
|
if direction: direction = direction.upper()
|
||
|
return log2vis(text, direction,clean=True) if direction in ('LTR','RTL') else text
|
||
|
from reportlab.lib.utils import KlassStore
|
||
|
from copy import deepcopy
|
||
|
_bidiKS = KlassStore()
|
||
|
def innerBidiStrWrap(s, bidiV=-1, bidiL=-1):
|
||
|
if isinstance(s,BidiStr): return s
|
||
|
if type(s) is str:
|
||
|
klass = BidiStr
|
||
|
else:
|
||
|
sklassName = s.__class__.__name__
|
||
|
klassName = f'BidiIndexed{sklassName}'
|
||
|
if klassName not in _bidiKS:
|
||
|
NS=dict(BidiStr=BidiStr,klassName=klassName,klass=s.__class__,deepcopy=deepcopy)
|
||
|
exec( f'''class {klassName}(klass,BidiStr):\n'''
|
||
|
'''\tdef __new__(cls,s,bidiV=-1,bidiL=-1):\n'''
|
||
|
'''\t\tself = super(cls,cls).__new__(cls,s)\n'''
|
||
|
'''\t\tif hasattr(s,'__dict__'): self.__dict__=deepcopy(s.__dict__)\n'''
|
||
|
'''\t\tself.__bidiV__ = bidiV\n'''
|
||
|
'''\t\tself.__bidiL__ = bidiL\n'''
|
||
|
'''\t\treturn self\n''',
|
||
|
NS)
|
||
|
_bidiKS.add(klassName,NS[klassName])
|
||
|
klass = _bidiKS[klassName]
|
||
|
return klass(s,bidiV=bidiV,bidiL=bidiL)
|
||
|
def bidiStrWrap(s, orig):
|
||
|
if not isinstance(orig,BidiStr): return s
|
||
|
return innerBidiStrWrap(s,orig.__bidiV__,orig.__bidiL__)
|
||
|
def bidiListWrap(L, orig):
|
||
|
if not isinstance(orig,(BidiList,BidiIndex)) or isinstance(L,BidiList): return L
|
||
|
if type(L) is list:
|
||
|
klass = BidiList
|
||
|
else:
|
||
|
lklassName = L.__class__.__name__
|
||
|
klassName = f'BidiIndexed{lklassName}'
|
||
|
if klassName not in _bidiKS:
|
||
|
NS=dict(BidiList=BidiList,klassName=klassName,klass=L.__class__,deepcopy=deepcopy)
|
||
|
exec( f'''class {klassName}(klass,BidiList):\n'''
|
||
|
'''\tdef __init__(self,L,bidiV=-1,bidiL=-1):\n'''
|
||
|
'''\t\tklass.__init__(self,L)\n'''
|
||
|
'''\t\tif hasattr(L,'__dict__'): self.__dict__=deepcopy(L.__dict__)\n'''
|
||
|
'''\t\tself.__bidiV__ = bidiV\n'''
|
||
|
'''\t\tself.__bidiL__ = bidiL\n''',
|
||
|
NS)
|
||
|
_bidiKS.add(klassName,NS[klassName])
|
||
|
klass = _bidiKS[klassName]
|
||
|
return klass(L,orig.__bidiV__,orig.__bidiL__)
|
||
|
#return _bidiKS[klassName](L,orig.__bidiV__,orig.__bidiL__)
|
||
|
class BidiIndex:
|
||
|
def __init__(self,bidiV=-1,bidiL=-1):
|
||
|
self.__bidiV__ = bidiV
|
||
|
self.__bidiL__ = bidiL
|
||
|
import re
|
||
|
wordpat = re.compile(r'[^ ]+',re.M)
|
||
|
del re
|
||
|
def bidiWordList(words,direction='RTL', clean=True, wx=False):
|
||
|
'''takes words (list of strings) returns bidi associated lists
|
||
|
if wx is True then the V2L index only is returned
|
||
|
'''
|
||
|
if direction: direction = direction.upper()
|
||
|
if direction not in ('LTR','RTL'): return words
|
||
|
if not isinstance(words,(list,tuple)):
|
||
|
raise ValueError('bidiWordList argument words should be a list or tuple of strings')
|
||
|
raw = ' '.join(words)
|
||
|
V2L = []
|
||
|
bidi = log2vis(raw, base_direction=direction, clean=clean, positions_V_to_L=V2L)
|
||
|
|
||
|
VMAP = {}
|
||
|
for i, m in enumerate(wordpat.finditer(bidi)):
|
||
|
t = (m.group(0), i)
|
||
|
start, end = m.span()
|
||
|
for j in range(start,end):
|
||
|
VMAP[V2L[j]] = t
|
||
|
|
||
|
res = [].append
|
||
|
#create result by assigning a V word to a raw one
|
||
|
for w, m in enumerate(wordpat.finditer(raw)):
|
||
|
for j in range(*m.span()):
|
||
|
if j in VMAP:
|
||
|
s, i = VMAP[j]
|
||
|
res(i if wx else BidiStr(s,i,w))
|
||
|
break
|
||
|
else:
|
||
|
#we seem to have a raw word that doesn't appear in bidi
|
||
|
pass
|
||
|
return res.__self__
|
||
|
def bidiShapedText(text, direction='RTL', clean=True, fontName='Helvetica', fontSize=10, shaping=False):
|
||
|
'''return shaped/bidi text and width; assumes text is aways in logical order'''
|
||
|
text = asUnicode(text)
|
||
|
if shaping:
|
||
|
font = pdfmetrics_getFont(fontName)
|
||
|
if not font.shapable: shaping = False
|
||
|
if direction: direction = direction.upper()
|
||
|
if direction in ('RTL','LTR'):
|
||
|
if shaping:
|
||
|
w = text.lstrip()
|
||
|
bL = len(text) - len(w)
|
||
|
text = w.rstrip()
|
||
|
bR = len(w) - len(text)
|
||
|
LW = [text[slice(*m.span())] for m in wordpat.finditer(text)] #logical words
|
||
|
VX = bidiWordList(LW,direction=direction,wx=True) #visual order
|
||
|
SW = [shapeStr(LW[i],fontName,fontSize) for i in VX] #shaped words in visual order
|
||
|
if len(VX)>1 and VX[0]>VX[-1]:
|
||
|
bL, bR = bR, bL
|
||
|
text = shapeStr(bL*' ',fontName,fontSize, force=True)
|
||
|
for w in SW:
|
||
|
if not hasattr(w,'__shapeData__'): w = shapeStr(w,fontName,fontSize,force=True)
|
||
|
if w is not SW[0]: text += shapeStr(' ',fontName,fontSize, force=True)
|
||
|
text += w
|
||
|
if bR:
|
||
|
text += shapeStr(bR*' ',fontName,fontSize)
|
||
|
else:
|
||
|
text = log2vis(text,base_direction=direction,clean=clean)
|
||
|
else:
|
||
|
if shaping:
|
||
|
text = shapeStr(text, fontName, fontSize)
|
||
|
width = (sum((_.x_advance for _ in text.__shapeData__))*fontSize/1000 if isinstance(text,ShapedStr)
|
||
|
else pdfmetrics_stringWidth(text, fontName, fontSize))
|
||
|
return text, width
|
||
|
def bidiFragWord(w,direction=None,bidiV=-1,bidiL=-1, clean=True):
|
||
|
if direction not in ('RTL','LTR'): return w
|
||
|
text = ''
|
||
|
cbd = [].append
|
||
|
fL = [].append
|
||
|
for i, (f, s) in enumerate(w[1:]):
|
||
|
if hasattr(f,'cbDefn'):
|
||
|
cbd(i)
|
||
|
else:
|
||
|
for u in s:
|
||
|
fL(i)
|
||
|
text += u
|
||
|
if len(text)<=1: return w #nothing to bidi
|
||
|
fL = fL.__self__
|
||
|
V2L = []
|
||
|
bidi = log2vis(text, base_direction=direction, clean=clean, positions_V_to_L=V2L)
|
||
|
fsL = [(w[1+k][0],''.join((ks[1] for ks in g))) for k, g in groupby(((fL[V2L[i]],v) for i,v in enumerate(bidi)),key=itemgetter(0))]
|
||
|
bfw = [sum((pdfmetrics_stringWidth(s,f.fontName,f.fontSize) for f,s in fsL))] + fsL
|
||
|
cbd = cbd.__self__
|
||
|
if cbd:
|
||
|
if V2L[0]>V2L[-1]:
|
||
|
#reversed
|
||
|
for i in sorted(cbd):
|
||
|
fs = w[i+1]
|
||
|
bfw.insert(len(w)-i,fs)
|
||
|
bfw[0] += getattr(fs,'width',0)
|
||
|
else:
|
||
|
for i in reversed(sorted(cbd)):
|
||
|
fs = w[i+1]
|
||
|
bfw.insert(i+1,fs)
|
||
|
bfw[0] += getattr(fs,'width',0)
|
||
|
bfw = w.__class__(bfw)
|
||
|
return bidiListWrap(bfw, BidiIndex(bidiV,bidiL))
|
||
|
rtlSupport = True
|
||
|
except:
|
||
|
import warnings
|
||
|
_rlbidiMsg = 'rlbidi is not installed - RTL/LTR not supported'
|
||
|
def log2vis(*args,**kwds):
|
||
|
raise ValueError(_rlbidiMsg)
|
||
|
isBidiStr = isBidiList = lambda _: False
|
||
|
BidiStr = str
|
||
|
BidiList = list
|
||
|
def bidiText(*args,**kwds):
|
||
|
direction = kwds.pop('direction',None)
|
||
|
if direction: direction = direction.upper()
|
||
|
if direction in ('LTR','RTL'):
|
||
|
warnings.warn(_rlbidiMsg,stacklevel=0)
|
||
|
return args[0]
|
||
|
def bidiShapedText(text, direction='RTL', clean=True, fontName='Helvetica', fontSize=10, shaping=False):
|
||
|
return bidiText(text,direction), pdfmetrics_stringWidth(text,fontName,fontSize)
|
||
|
bidiWordList = bidiStrWrap = bidiListWrap = bidiFragWord = innerBidiStrWrap = BidiIndex = bidiText
|
||
|
rtlSupport = False
|
||
|
|
||
|
class _PDFColorSetter:
|
||
|
'''Abstracts the color setting operations; used in Canvas and Textobject
|
||
|
asseumes we have a _code object'''
|
||
|
def _checkSeparation(self,cmyk):
|
||
|
if isinstance(cmyk,CMYKColorSep):
|
||
|
name,sname = self._doc.addColor(cmyk)
|
||
|
if name not in self._colorsUsed:
|
||
|
self._colorsUsed[name] = sname
|
||
|
return name
|
||
|
|
||
|
#if this is set to a callable(color) --> color it can be used to check color setting
|
||
|
#see eg _enforceCMYK/_enforceRGB
|
||
|
_enforceColorSpace = None
|
||
|
|
||
|
def setFillColorCMYK(self, c, m, y, k, alpha=None):
|
||
|
"""set the fill color useing negative color values
|
||
|
(cyan, magenta, yellow and darkness value).
|
||
|
Takes 4 arguments between 0.0 and 1.0"""
|
||
|
self.setFillColor((c,m,y,k),alpha=alpha)
|
||
|
|
||
|
def setStrokeColorCMYK(self, c, m, y, k, alpha=None):
|
||
|
"""set the stroke color useing negative color values
|
||
|
(cyan, magenta, yellow and darkness value).
|
||
|
Takes 4 arguments between 0.0 and 1.0"""
|
||
|
self.setStrokeColor((c,m,y,k),alpha=alpha)
|
||
|
|
||
|
def setFillColorRGB(self, r, g, b, alpha=None):
|
||
|
"""Set the fill color using positive color description
|
||
|
(Red,Green,Blue). Takes 3 arguments between 0.0 and 1.0"""
|
||
|
self.setFillColor((r,g,b),alpha=alpha)
|
||
|
|
||
|
def setStrokeColorRGB(self, r, g, b, alpha=None):
|
||
|
"""Set the stroke color using positive color description
|
||
|
(Red,Green,Blue). Takes 3 arguments between 0.0 and 1.0"""
|
||
|
self.setStrokeColor((r,g,b),alpha=alpha)
|
||
|
|
||
|
def setFillColor(self, aColor, alpha=None):
|
||
|
"""Takes a color object, allowing colors to be referred to by name"""
|
||
|
if self._enforceColorSpace:
|
||
|
aColor = self._enforceColorSpace(aColor)
|
||
|
if isinstance(aColor, CMYKColor):
|
||
|
d = aColor.density
|
||
|
c,m,y,k = (d*aColor.cyan, d*aColor.magenta, d*aColor.yellow, d*aColor.black)
|
||
|
self._fillColorObj = aColor
|
||
|
name = self._checkSeparation(aColor)
|
||
|
if name:
|
||
|
self._code.append('/%s cs %s scn' % (name,fp_str(d)))
|
||
|
else:
|
||
|
self._code.append('%s k' % fp_str(c, m, y, k))
|
||
|
elif isinstance(aColor, Color):
|
||
|
rgb = (aColor.red, aColor.green, aColor.blue)
|
||
|
self._fillColorObj = aColor
|
||
|
self._code.append('%s rg' % fp_str(rgb) )
|
||
|
elif isinstance(aColor,(tuple,list)):
|
||
|
l = len(aColor)
|
||
|
if l==3:
|
||
|
self._fillColorObj = aColor
|
||
|
self._code.append('%s rg' % fp_str(aColor) )
|
||
|
elif l==4:
|
||
|
self._fillColorObj = aColor
|
||
|
self._code.append('%s k' % fp_str(aColor))
|
||
|
else:
|
||
|
raise ValueError('Unknown color %r' % aColor)
|
||
|
elif isStr(aColor):
|
||
|
self.setFillColor(toColor(aColor))
|
||
|
else:
|
||
|
raise ValueError('Unknown color %r' % aColor)
|
||
|
if alpha is not None:
|
||
|
self.setFillAlpha(alpha)
|
||
|
elif getattr(aColor, 'alpha', None) is not None:
|
||
|
self.setFillAlpha(aColor.alpha)
|
||
|
|
||
|
def setStrokeColor(self, aColor, alpha=None):
|
||
|
"""Takes a color object, allowing colors to be referred to by name"""
|
||
|
if self._enforceColorSpace:
|
||
|
aColor = self._enforceColorSpace(aColor)
|
||
|
if isinstance(aColor, CMYKColor):
|
||
|
d = aColor.density
|
||
|
c,m,y,k = (d*aColor.cyan, d*aColor.magenta, d*aColor.yellow, d*aColor.black)
|
||
|
self._strokeColorObj = aColor
|
||
|
name = self._checkSeparation(aColor)
|
||
|
if name:
|
||
|
self._code.append('/%s CS %s SCN' % (name,fp_str(d)))
|
||
|
else:
|
||
|
self._code.append('%s K' % fp_str(c, m, y, k))
|
||
|
elif isinstance(aColor, Color):
|
||
|
rgb = (aColor.red, aColor.green, aColor.blue)
|
||
|
self._strokeColorObj = aColor
|
||
|
self._code.append('%s RG' % fp_str(rgb) )
|
||
|
elif isinstance(aColor,(tuple,list)):
|
||
|
l = len(aColor)
|
||
|
if l==3:
|
||
|
self._strokeColorObj = aColor
|
||
|
self._code.append('%s RG' % fp_str(aColor) )
|
||
|
elif l==4:
|
||
|
self._strokeColorObj = aColor
|
||
|
self._code.append('%s K' % fp_str(aColor))
|
||
|
else:
|
||
|
raise ValueError('Unknown color %r' % aColor)
|
||
|
elif isStr(aColor):
|
||
|
self.setStrokeColor(toColor(aColor))
|
||
|
else:
|
||
|
raise ValueError('Unknown color %r' % aColor)
|
||
|
if alpha is not None:
|
||
|
self.setStrokeAlpha(alpha)
|
||
|
elif getattr(aColor, 'alpha', None) is not None:
|
||
|
self.setStrokeAlpha(aColor.alpha)
|
||
|
|
||
|
def setFillGray(self, gray, alpha=None):
|
||
|
"""Sets the gray level; 0.0=black, 1.0=white"""
|
||
|
self._fillColorObj = (gray, gray, gray)
|
||
|
self._code.append('%s g' % fp_str(gray))
|
||
|
if alpha is not None:
|
||
|
self.setFillAlpha(alpha)
|
||
|
|
||
|
def setStrokeGray(self, gray, alpha=None):
|
||
|
"""Sets the gray level; 0.0=black, 1.0=white"""
|
||
|
self._strokeColorObj = (gray, gray, gray)
|
||
|
self._code.append('%s G' % fp_str(gray))
|
||
|
if alpha is not None:
|
||
|
self.setFillAlpha(alpha)
|
||
|
|
||
|
def setStrokeAlpha(self,a):
|
||
|
if not (isinstance(a,(float,int)) and 0<=a<=1):
|
||
|
raise ValueError('setStrokeAlpha invalid value %r' % a)
|
||
|
getattr(self,'_setStrokeAlpha',lambda x: None)(a)
|
||
|
|
||
|
def setFillAlpha(self,a):
|
||
|
if not (isinstance(a,(float,int)) and 0<=a<=1):
|
||
|
raise ValueError('setFillAlpha invalid value %r' % a)
|
||
|
getattr(self,'_setFillAlpha',lambda x: None)(a)
|
||
|
|
||
|
def setStrokeOverprint(self,a):
|
||
|
getattr(self,'_setStrokeOverprint',lambda x: None)(a)
|
||
|
|
||
|
def setFillOverprint(self,a):
|
||
|
getattr(self,'_setFillOverprint',lambda x: None)(a)
|
||
|
|
||
|
def setOverprintMask(self,a):
|
||
|
getattr(self,'_setOverprintMask',lambda x: None)(a)
|
||
|
|
||
|
class PDFTextObject(_PDFColorSetter):
|
||
|
"""PDF logically separates text and graphics drawing; text
|
||
|
operations need to be bracketed between BT (Begin text) and
|
||
|
ET operators. This class ensures text operations are
|
||
|
properly encapusalted. Ask the canvas for a text object
|
||
|
with beginText(x, y). Do not construct one directly.
|
||
|
Do not use multiple text objects in parallel; PDF is
|
||
|
not multi-threaded!
|
||
|
|
||
|
It keeps track of x and y coordinates relative to its origin."""
|
||
|
|
||
|
def __init__(self, canvas, x=0,y=0, direction=None):
|
||
|
self._code = ['BT'] #no point in [] then append RGB
|
||
|
self._canvas = canvas #canvas sets this so it has access to size info
|
||
|
self._fontname = self._canvas._fontname
|
||
|
self._fontsize = self._canvas._fontsize
|
||
|
self._leading = self._canvas._leading
|
||
|
self._doc = self._canvas._doc
|
||
|
self._colorsUsed = self._canvas._colorsUsed
|
||
|
self._enforceColorSpace = getattr(canvas,'_enforceColorSpace',None)
|
||
|
font = pdfmetrics_getFont(self._fontname)
|
||
|
self._curSubset = -1
|
||
|
self.direction = direction
|
||
|
self.setTextOrigin(x, y)
|
||
|
self._textRenderMode = 0
|
||
|
self._clipping = 0
|
||
|
self._rise = 0
|
||
|
|
||
|
def getCode(self):
|
||
|
"pack onto one line; used internally"
|
||
|
self._code.append('ET')
|
||
|
if self._clipping:
|
||
|
self._code.append('%d Tr' % (self._textRenderMode^4))
|
||
|
return ' '.join(self._code)
|
||
|
|
||
|
def setTextOrigin(self, x, y):
|
||
|
if self._canvas.bottomup:
|
||
|
self._code.append('1 0 0 1 %s Tm' % fp_str(x, y)) #bottom up
|
||
|
else:
|
||
|
self._code.append('1 0 0 -1 %s Tm' % fp_str(x, y)) #top down
|
||
|
|
||
|
# The current cursor position is at the text origin
|
||
|
self._x0 = self._x = x
|
||
|
self._y0 = self._y = y
|
||
|
|
||
|
def setTextTransform(self, a, b, c, d, e, f):
|
||
|
"Like setTextOrigin, but does rotation, scaling etc."
|
||
|
if not self._canvas.bottomup:
|
||
|
c = -c #reverse bottom row of the 2D Transform
|
||
|
d = -d
|
||
|
self._code.append('%s Tm' % fp_str(a, b, c, d, e, f))
|
||
|
|
||
|
# The current cursor position is at the text origin Note that
|
||
|
# we aren't keeping track of all the transform on these
|
||
|
# coordinates: they are relative to the rotations/sheers
|
||
|
# defined in the matrix.
|
||
|
self._x0 = self._x = e
|
||
|
self._y0 = self._y = f
|
||
|
|
||
|
def moveCursor(self, dx, dy):
|
||
|
"""Starts a new line at an offset dx,dy from the start of the
|
||
|
current line. This does not move the cursor relative to the
|
||
|
current position, and it changes the current offset of every
|
||
|
future line drawn (i.e. if you next do a textLine() call, it
|
||
|
will move the cursor to a position one line lower than the
|
||
|
position specificied in this call. """
|
||
|
|
||
|
# Check if we have a previous move cursor call, and combine
|
||
|
# them if possible.
|
||
|
if self._code and self._code[-1].endswith(' Td'):
|
||
|
L = self._code[-1].split()
|
||
|
if len(L)==3:
|
||
|
del self._code[-1]
|
||
|
else:
|
||
|
self._code[-1] = ''.join(L[:-4])
|
||
|
|
||
|
# Work out the last movement
|
||
|
lastDx = float(L[-3])
|
||
|
lastDy = float(L[-2])
|
||
|
|
||
|
# Combine the two movement
|
||
|
dx += lastDx
|
||
|
dy -= lastDy
|
||
|
|
||
|
# We will soon add the movement to the line origin, so if
|
||
|
# we've already done this for lastDx, lastDy, remove it
|
||
|
# first (so it will be right when added back again).
|
||
|
self._x0 -= lastDx
|
||
|
self._y0 -= lastDy
|
||
|
|
||
|
# Output the move text cursor call.
|
||
|
self._code.append('%s Td' % fp_str(dx, -dy))
|
||
|
|
||
|
# Keep track of the new line offsets and the cursor position
|
||
|
self._x0 += dx
|
||
|
self._y0 += dy
|
||
|
self._x = self._x0
|
||
|
self._y = self._y0
|
||
|
|
||
|
def setXPos(self, dx):
|
||
|
"""Starts a new line dx away from the start of the
|
||
|
current line - NOT from the current point! So if
|
||
|
you call it in mid-sentence, watch out."""
|
||
|
self.moveCursor(dx,0)
|
||
|
|
||
|
def getCursor(self):
|
||
|
"""Returns current text position relative to the last origin."""
|
||
|
return (self._x, self._y)
|
||
|
|
||
|
def getStartOfLine(self):
|
||
|
"""Returns a tuple giving the text position of the start of the
|
||
|
current line."""
|
||
|
return (self._x0, self._y0)
|
||
|
|
||
|
def getX(self):
|
||
|
"""Returns current x position relative to the last origin."""
|
||
|
return self._x
|
||
|
|
||
|
def getY(self):
|
||
|
"""Returns current y position relative to the last origin."""
|
||
|
return self._y
|
||
|
|
||
|
def _setFont(self, psfontname, size):
|
||
|
"""Sets the font and fontSize
|
||
|
Raises a readable exception if an illegal font
|
||
|
is supplied. Font names are case-sensitive! Keeps track
|
||
|
of font anme and size for metrics."""
|
||
|
self._fontname = psfontname
|
||
|
self._fontsize = size
|
||
|
font = pdfmetrics_getFont(self._fontname)
|
||
|
|
||
|
if font._dynamicFont:
|
||
|
self._curSubset = -1
|
||
|
else:
|
||
|
pdffontname = self._canvas._doc.getInternalFontName(psfontname)
|
||
|
self._code.append('%s %s Tf' % (pdffontname, fp_str(size)))
|
||
|
|
||
|
def setFont(self, psfontname, size, leading = None):
|
||
|
"""Sets the font. If leading not specified, defaults to 1.2 x
|
||
|
font size. Raises a readable exception if an illegal font
|
||
|
is supplied. Font names are case-sensitive! Keeps track
|
||
|
of font anme and size for metrics."""
|
||
|
self._fontname = psfontname
|
||
|
self._fontsize = size
|
||
|
if leading is None:
|
||
|
leading = size * 1.2
|
||
|
self._leading = leading
|
||
|
font = pdfmetrics_getFont(self._fontname)
|
||
|
if font._dynamicFont:
|
||
|
self._curSubset = -1
|
||
|
else:
|
||
|
pdffontname = self._canvas._doc.getInternalFontName(psfontname)
|
||
|
self._code.append('%s %s Tf %s TL' % (pdffontname, fp_str(size), fp_str(leading)))
|
||
|
|
||
|
def setCharSpace(self, charSpace):
|
||
|
"""Adjusts inter-character spacing"""
|
||
|
self._charSpace = charSpace
|
||
|
self._code.append('%s Tc' % fp_str(charSpace))
|
||
|
|
||
|
def setWordSpace(self, wordSpace):
|
||
|
"""Adjust inter-word spacing. This can be used
|
||
|
to flush-justify text - you get the width of the
|
||
|
words, and add some space between them."""
|
||
|
self._wordSpace = wordSpace
|
||
|
self._code.append('%s Tw' % fp_str(wordSpace))
|
||
|
|
||
|
def setHorizScale(self, horizScale):
|
||
|
"Stretches text out horizontally"
|
||
|
self._horizScale = 100 + horizScale
|
||
|
self._code.append('%s Tz' % fp_str(horizScale))
|
||
|
|
||
|
def setLeading(self, leading):
|
||
|
"How far to move down at the end of a line."
|
||
|
self._leading = leading
|
||
|
self._code.append('%s TL' % fp_str(leading))
|
||
|
|
||
|
def setTextRenderMode(self, mode):
|
||
|
"""Set the text rendering mode.
|
||
|
|
||
|
0 = Fill text
|
||
|
1 = Stroke text
|
||
|
2 = Fill then stroke
|
||
|
3 = Invisible
|
||
|
4 = Fill text and add to clipping path
|
||
|
5 = Stroke text and add to clipping path
|
||
|
6 = Fill then stroke and add to clipping path
|
||
|
7 = Add to clipping path
|
||
|
|
||
|
after we start clipping we mustn't change the mode back until after the ET
|
||
|
"""
|
||
|
|
||
|
assert mode in (0,1,2,3,4,5,6,7), "mode must be in (0,1,2,3,4,5,6,7)"
|
||
|
if (mode & 4)!=self._clipping:
|
||
|
mode |= 4
|
||
|
self._clipping = mode & 4
|
||
|
if self._textRenderMode!=mode:
|
||
|
self._textRenderMode = mode
|
||
|
self._code.append('%d Tr' % mode)
|
||
|
|
||
|
def setRise(self, rise):
|
||
|
"Move text baseline up or down to allow superscript/subscripts"
|
||
|
v = f'{fp_str(rise)} Ts'
|
||
|
if self._code[-1].endswith(' Ts'): #optimize out r0 Ts r1 Ts
|
||
|
#reverse previous changes
|
||
|
self._y += self._rise
|
||
|
self._code[-1] = v
|
||
|
else:
|
||
|
self._rise = rise
|
||
|
self._y -= rise
|
||
|
self._code.append(v)
|
||
|
|
||
|
def _formatText(self, text):
|
||
|
"Generates PDF text output operator(s)"
|
||
|
#if log2vis and self.direction in ('LTR','RTL'):
|
||
|
# # Use pyfribidi to write the text in the correct visual order.
|
||
|
# text = log2vis(text, self.direction)
|
||
|
canv = self._canvas
|
||
|
font = pdfmetrics_getFont(self._fontname)
|
||
|
state = (self._code, self._x, self._y)
|
||
|
try:
|
||
|
self._code = []
|
||
|
R = self._code.append
|
||
|
if font._dynamicFont:
|
||
|
canv_escape = canv._escape
|
||
|
tmpl = None
|
||
|
r0 = self._rise
|
||
|
#it's a truetype font
|
||
|
if font.shapable and isinstance(text,ShapedStr):
|
||
|
sd0 = 0
|
||
|
r0 = self._rise
|
||
|
shapeData = text.__shapeData__
|
||
|
fontsize = self._fontsize
|
||
|
for subset, t in font.splitString(text, canv._doc):
|
||
|
cluster = None
|
||
|
if subset!=self._curSubset:
|
||
|
if not tmpl:
|
||
|
tmpl = f'{fp_str(fontsize)} Tf {fp_str(self._leading)} TL'
|
||
|
R(f'{font.getSubsetInternalName(subset, canv._doc)} {tmpl}')
|
||
|
self._curSubset = subset
|
||
|
sd1 = sd0 + len(t)
|
||
|
SD = shapeData[sd0:sd1] + _sdGuardL
|
||
|
sd0 = sd1
|
||
|
for i, sd in enumerate(SD):
|
||
|
r = r0 + fontsize*sd.y_offset/1000
|
||
|
if cluster is None or sd.cluster<0 or r!=self._rise:
|
||
|
if cluster is not None:
|
||
|
#end current cluster
|
||
|
A = [v for v in ((('(%s)' % canv_escape(b''.join(g))) if k else fp_str(sum(g)))
|
||
|
for k, g in groupby(filter(None,A.__self__),lambda x: isinstance(x,bytes))) if v!='0']
|
||
|
if len(A)==1 and A[0].startswith('('):
|
||
|
R(f'{A[0]} Tj')
|
||
|
else:
|
||
|
R(f'[{" ".join(A)}] TJ')
|
||
|
|
||
|
if self._rise!=r0: self.setRise(r0)
|
||
|
if sd.cluster<0: break
|
||
|
|
||
|
#begin new cluster
|
||
|
if r!=self._rise: self.setRise(r)
|
||
|
cluster = sd.cluster
|
||
|
A = [].append
|
||
|
|
||
|
#we assume that both harfbuzz and pdf positions are correct
|
||
|
A(-sd.x_offset) #adjust using harfbuzz offset
|
||
|
A(bytes(chr(t[i]).encode('latin1')))
|
||
|
#A(sd.x_offset) #remove the harfbuzz adjustment
|
||
|
#we assume the harfbuzz position is correct, but we will have
|
||
|
# 1<----O------|
|
||
|
# 1-----O ---->|--------------A--------->2
|
||
|
#
|
||
|
# 1<----O------|<....(W-O).....><...x...>2
|
||
|
# 1--------------------W------->
|
||
|
#
|
||
|
# W - O + x = A ==> x = A-W+O
|
||
|
A(sd.width - sd.x_advance + sd.x_offset)
|
||
|
if self._rise!=r0: self.setRise(r0)
|
||
|
else:
|
||
|
for subset, t in font.splitString(text, canv._doc):
|
||
|
if subset!=self._curSubset:
|
||
|
if not tmpl:
|
||
|
tmpl = f'{fp_str(self._fontsize)} Tf {fp_str(self._leading)} TL'
|
||
|
R(f'{font.getSubsetInternalName(subset, canv._doc)} {tmpl}')
|
||
|
self._curSubset = subset
|
||
|
R(f'({canv_escape(t)}) Tj')
|
||
|
elif font._multiByte:
|
||
|
#all the fonts should really work like this - let them know more about PDF...
|
||
|
R("%s %s Tf %s TL" % (
|
||
|
canv._doc.getInternalFontName(font.fontName),
|
||
|
fp_str(self._fontsize),
|
||
|
fp_str(self._leading)
|
||
|
))
|
||
|
R("(%s) Tj" % font.formatForPdf(text))
|
||
|
else:
|
||
|
#convert to T1 coding
|
||
|
fc = font
|
||
|
if isBytes(text):
|
||
|
try:
|
||
|
text = text.decode('utf8')
|
||
|
except UnicodeDecodeError as e:
|
||
|
i,j = e.args[2:4]
|
||
|
raise UnicodeDecodeError(*(e.args[:4]+('%s\n%s-->%s<--%s' % (e.args[4],text[max(i-10,0):i],text[i:j],text[j:j+10]),)))
|
||
|
|
||
|
canv_escape = canv._escape
|
||
|
for f, t in pdfmetrics_unicode2T1(text,[font]+font.substitutionFonts):
|
||
|
if f!=fc:
|
||
|
R("%s %s Tf %s TL" % (canv._doc.getInternalFontName(f.fontName), fp_str(self._fontsize), fp_str(self._leading)))
|
||
|
fc = f
|
||
|
R(f'({canv_escape(t)}) Tj')
|
||
|
if font!=fc:
|
||
|
R("%s %s Tf %s TL" % (canv._doc.getInternalFontName(self._fontname), fp_str(self._fontsize), fp_str(self._leading)))
|
||
|
finally:
|
||
|
self._code, self._x, self._y = state
|
||
|
return ' '.join(R.__self__)
|
||
|
|
||
|
def _shapedTextOut(self, text, dx, dy):
|
||
|
add = self._code.append
|
||
|
canv = self._canvas
|
||
|
font = pdfmetrics_getFont(self._fontname)
|
||
|
canv_escape = canv._escape
|
||
|
for subset, t in font.splitString(text, canv._doc):
|
||
|
if subset!=self._curSubset:
|
||
|
pdffontname = font.getSubsetInternalName(subset, canv._doc)
|
||
|
R.append("%s %s Tf %s TL" % (pdffontname, fp_str(self._fontsize), fp_str(self._leading)))
|
||
|
self._curSubset = subset
|
||
|
if dy:
|
||
|
print(f'{dy} -->',end='')
|
||
|
dy = (dy / font.face.unitsPerEm) * self._fontsize
|
||
|
print(f'{dy}')
|
||
|
add(f'{fp_str(dy)} Ts')
|
||
|
if dx:
|
||
|
add(f'[{fp_str(font.pdfScale(dx))} ({canv_escape(t)})] TJ')
|
||
|
if dy:
|
||
|
add(f'{fp_str(-dy)} Ts')
|
||
|
|
||
|
|
||
|
def _textOut(self, text, TStar=0):
|
||
|
"prints string at current point, ignores text cursor"
|
||
|
self._code.append('%s%s' % (self._formatText(text), (TStar and ' T*' or '')))
|
||
|
|
||
|
def textOut(self, text):
|
||
|
"""prints string at current point, text cursor moves across."""
|
||
|
self._x = self._x + self._canvas.stringWidth(text, self._fontname, self._fontsize)
|
||
|
self._code.append(self._formatText(text))
|
||
|
|
||
|
def textLine(self, text=''):
|
||
|
"""prints string at current point, text cursor moves down.
|
||
|
Can work with no argument to simply move the cursor down."""
|
||
|
# Update the coordinates of the cursor
|
||
|
self._x = self._x0
|
||
|
if self._canvas.bottomup:
|
||
|
self._y = self._y - self._leading
|
||
|
else:
|
||
|
self._y = self._y + self._leading
|
||
|
|
||
|
# Update the location of the start of the line
|
||
|
# self._x0 is unchanged
|
||
|
self._y0 = self._y
|
||
|
|
||
|
# Output the text followed by a PDF newline command
|
||
|
self._code.append('%s T*' % self._formatText(text))
|
||
|
|
||
|
def textLines(self, stuff, trim=1):
|
||
|
"""prints multi-line or newlined strings, moving down. One
|
||
|
comon use is to quote a multi-line block in your Python code;
|
||
|
since this may be indented, by default it trims whitespace
|
||
|
off each line and from the beginning; set trim=0 to preserve
|
||
|
whitespace."""
|
||
|
if isStr(stuff):
|
||
|
lines = asUnicode(stuff).strip().split(u'\n')
|
||
|
if trim==1:
|
||
|
lines = [s.strip() for s in lines]
|
||
|
elif isinstance(stuff,(tuple,list)):
|
||
|
lines = stuff
|
||
|
else:
|
||
|
assert 1==0, "argument to textlines must be string,, list or tuple"
|
||
|
|
||
|
# Output each line one at a time. This used to be a long-hand
|
||
|
# copy of the textLine code, now called as a method.
|
||
|
for line in lines:
|
||
|
self.textLine(line)
|
||
|
|
||
|
def __nonzero__(self):
|
||
|
'PDFTextObject is true if it has something done after the init'
|
||
|
return self._code != ['BT']
|
||
|
|
||
|
def _setFillAlpha(self,v):
|
||
|
self._canvas._doc.ensureMinPdfVersion('transparency')
|
||
|
self._canvas._extgstate.set(self,'ca',v)
|
||
|
|
||
|
def _setStrokeOverprint(self,v):
|
||
|
self._canvas._extgstate.set(self,'OP',v)
|
||
|
|
||
|
def _setFillOverprint(self,v):
|
||
|
self._canvas._extgstate.set(self,'op',v)
|
||
|
|
||
|
def _setOverprintMask(self,v):
|
||
|
self._canvas._extgstate.set(self,'OPM',v and 1 or 0)
|