textobject.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. #Copyright ReportLab Europe Ltd. 2000-2017
  2. #see license.txt for license details
  3. #history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/pdfgen/textobject.py
  4. __version__='3.3.0'
  5. __doc__="""
  6. PDFTextObject is an efficient way to add text to a Canvas. Do not
  7. instantiate directly, obtain one from the Canvas instead.
  8. Progress Reports:
  9. 8.83, 2000-01-13, gmcm: created from pdfgen.py
  10. """
  11. import string
  12. from types import *
  13. from reportlab.lib.colors import Color, CMYKColor, CMYKColorSep, toColor, black, white, _CMYK_black, _CMYK_white
  14. from reportlab.lib.utils import isBytes, isStr, asUnicode
  15. from reportlab.lib.rl_accel import fp_str
  16. from reportlab.pdfbase import pdfmetrics
  17. from reportlab.rl_config import rtlSupport
  18. log2vis = None
  19. def fribidiText(text,direction):
  20. return text
  21. if rtlSupport:
  22. try:
  23. from pyfribidi2 import log2vis, ON as DIR_ON, LTR as DIR_LTR, RTL as DIR_RTL
  24. directionsMap = dict(LTR=DIR_LTR,RTL=DIR_RTL)
  25. def fribidiText(text,direction):
  26. return log2vis(text, directionsMap.get(direction,DIR_ON),clean=True) if direction in ('LTR','RTL') else text
  27. except:
  28. import warnings
  29. warnings.warn('pyfribidi is not installed - RTL not supported')
  30. class _PDFColorSetter:
  31. '''Abstracts the color setting operations; used in Canvas and Textobject
  32. asseumes we have a _code object'''
  33. def _checkSeparation(self,cmyk):
  34. if isinstance(cmyk,CMYKColorSep):
  35. name,sname = self._doc.addColor(cmyk)
  36. if name not in self._colorsUsed:
  37. self._colorsUsed[name] = sname
  38. return name
  39. #if this is set to a callable(color) --> color it can be used to check color setting
  40. #see eg _enforceCMYK/_enforceRGB
  41. _enforceColorSpace = None
  42. def setFillColorCMYK(self, c, m, y, k, alpha=None):
  43. """set the fill color useing negative color values
  44. (cyan, magenta, yellow and darkness value).
  45. Takes 4 arguments between 0.0 and 1.0"""
  46. self.setFillColor((c,m,y,k),alpha=alpha)
  47. def setStrokeColorCMYK(self, c, m, y, k, alpha=None):
  48. """set the stroke color useing negative color values
  49. (cyan, magenta, yellow and darkness value).
  50. Takes 4 arguments between 0.0 and 1.0"""
  51. self.setStrokeColor((c,m,y,k),alpha=alpha)
  52. def setFillColorRGB(self, r, g, b, alpha=None):
  53. """Set the fill color using positive color description
  54. (Red,Green,Blue). Takes 3 arguments between 0.0 and 1.0"""
  55. self.setFillColor((r,g,b),alpha=alpha)
  56. def setStrokeColorRGB(self, r, g, b, alpha=None):
  57. """Set the stroke color using positive color description
  58. (Red,Green,Blue). Takes 3 arguments between 0.0 and 1.0"""
  59. self.setStrokeColor((r,g,b),alpha=alpha)
  60. def setFillColor(self, aColor, alpha=None):
  61. """Takes a color object, allowing colors to be referred to by name"""
  62. if self._enforceColorSpace:
  63. aColor = self._enforceColorSpace(aColor)
  64. if isinstance(aColor, CMYKColor):
  65. d = aColor.density
  66. c,m,y,k = (d*aColor.cyan, d*aColor.magenta, d*aColor.yellow, d*aColor.black)
  67. self._fillColorObj = aColor
  68. name = self._checkSeparation(aColor)
  69. if name:
  70. self._code.append('/%s cs %s scn' % (name,fp_str(d)))
  71. else:
  72. self._code.append('%s k' % fp_str(c, m, y, k))
  73. elif isinstance(aColor, Color):
  74. rgb = (aColor.red, aColor.green, aColor.blue)
  75. self._fillColorObj = aColor
  76. self._code.append('%s rg' % fp_str(rgb) )
  77. elif isinstance(aColor,(tuple,list)):
  78. l = len(aColor)
  79. if l==3:
  80. self._fillColorObj = aColor
  81. self._code.append('%s rg' % fp_str(aColor) )
  82. elif l==4:
  83. self._fillColorObj = aColor
  84. self._code.append('%s k' % fp_str(aColor))
  85. else:
  86. raise ValueError('Unknown color %r' % aColor)
  87. elif isStr(aColor):
  88. self.setFillColor(toColor(aColor))
  89. else:
  90. raise ValueError('Unknown color %r' % aColor)
  91. if alpha is not None:
  92. self.setFillAlpha(alpha)
  93. elif getattr(aColor, 'alpha', None) is not None:
  94. self.setFillAlpha(aColor.alpha)
  95. def setStrokeColor(self, aColor, alpha=None):
  96. """Takes a color object, allowing colors to be referred to by name"""
  97. if self._enforceColorSpace:
  98. aColor = self._enforceColorSpace(aColor)
  99. if isinstance(aColor, CMYKColor):
  100. d = aColor.density
  101. c,m,y,k = (d*aColor.cyan, d*aColor.magenta, d*aColor.yellow, d*aColor.black)
  102. self._strokeColorObj = aColor
  103. name = self._checkSeparation(aColor)
  104. if name:
  105. self._code.append('/%s CS %s SCN' % (name,fp_str(d)))
  106. else:
  107. self._code.append('%s K' % fp_str(c, m, y, k))
  108. elif isinstance(aColor, Color):
  109. rgb = (aColor.red, aColor.green, aColor.blue)
  110. self._strokeColorObj = aColor
  111. self._code.append('%s RG' % fp_str(rgb) )
  112. elif isinstance(aColor,(tuple,list)):
  113. l = len(aColor)
  114. if l==3:
  115. self._strokeColorObj = aColor
  116. self._code.append('%s RG' % fp_str(aColor) )
  117. elif l==4:
  118. self._strokeColorObj = aColor
  119. self._code.append('%s K' % fp_str(aColor))
  120. else:
  121. raise ValueError('Unknown color %r' % aColor)
  122. elif isStr(aColor):
  123. self.setStrokeColor(toColor(aColor))
  124. else:
  125. raise ValueError('Unknown color %r' % aColor)
  126. if alpha is not None:
  127. self.setStrokeAlpha(alpha)
  128. elif getattr(aColor, 'alpha', None) is not None:
  129. self.setStrokeAlpha(aColor.alpha)
  130. def setFillGray(self, gray, alpha=None):
  131. """Sets the gray level; 0.0=black, 1.0=white"""
  132. self._fillColorObj = (gray, gray, gray)
  133. self._code.append('%s g' % fp_str(gray))
  134. if alpha is not None:
  135. self.setFillAlpha(alpha)
  136. def setStrokeGray(self, gray, alpha=None):
  137. """Sets the gray level; 0.0=black, 1.0=white"""
  138. self._strokeColorObj = (gray, gray, gray)
  139. self._code.append('%s G' % fp_str(gray))
  140. if alpha is not None:
  141. self.setFillAlpha(alpha)
  142. def setStrokeAlpha(self,a):
  143. if not (isinstance(a,(float,int)) and 0<=a<=1):
  144. raise ValueError('setStrokeAlpha invalid value %r' % a)
  145. getattr(self,'_setStrokeAlpha',lambda x: None)(a)
  146. def setFillAlpha(self,a):
  147. if not (isinstance(a,(float,int)) and 0<=a<=1):
  148. raise ValueError('setFillAlpha invalid value %r' % a)
  149. getattr(self,'_setFillAlpha',lambda x: None)(a)
  150. def setStrokeOverprint(self,a):
  151. getattr(self,'_setStrokeOverprint',lambda x: None)(a)
  152. def setFillOverprint(self,a):
  153. getattr(self,'_setFillOverprint',lambda x: None)(a)
  154. def setOverprintMask(self,a):
  155. getattr(self,'_setOverprintMask',lambda x: None)(a)
  156. class PDFTextObject(_PDFColorSetter):
  157. """PDF logically separates text and graphics drawing; text
  158. operations need to be bracketed between BT (Begin text) and
  159. ET operators. This class ensures text operations are
  160. properly encapusalted. Ask the canvas for a text object
  161. with beginText(x, y). Do not construct one directly.
  162. Do not use multiple text objects in parallel; PDF is
  163. not multi-threaded!
  164. It keeps track of x and y coordinates relative to its origin."""
  165. def __init__(self, canvas, x=0,y=0, direction=None):
  166. self._code = ['BT'] #no point in [] then append RGB
  167. self._canvas = canvas #canvas sets this so it has access to size info
  168. self._fontname = self._canvas._fontname
  169. self._fontsize = self._canvas._fontsize
  170. self._leading = self._canvas._leading
  171. self._doc = self._canvas._doc
  172. self._colorsUsed = self._canvas._colorsUsed
  173. self._enforceColorSpace = getattr(canvas,'_enforceColorSpace',None)
  174. font = pdfmetrics.getFont(self._fontname)
  175. self._curSubset = -1
  176. self.direction = direction
  177. self.setTextOrigin(x, y)
  178. self._textRenderMode = 0
  179. self._clipping = 0
  180. def getCode(self):
  181. "pack onto one line; used internally"
  182. self._code.append('ET')
  183. if self._clipping:
  184. self._code.append('%d Tr' % (self._textRenderMode^4))
  185. return ' '.join(self._code)
  186. def setTextOrigin(self, x, y):
  187. if self._canvas.bottomup:
  188. self._code.append('1 0 0 1 %s Tm' % fp_str(x, y)) #bottom up
  189. else:
  190. self._code.append('1 0 0 -1 %s Tm' % fp_str(x, y)) #top down
  191. # The current cursor position is at the text origin
  192. self._x0 = self._x = x
  193. self._y0 = self._y = y
  194. def setTextTransform(self, a, b, c, d, e, f):
  195. "Like setTextOrigin, but does rotation, scaling etc."
  196. if not self._canvas.bottomup:
  197. c = -c #reverse bottom row of the 2D Transform
  198. d = -d
  199. self._code.append('%s Tm' % fp_str(a, b, c, d, e, f))
  200. # The current cursor position is at the text origin Note that
  201. # we aren't keeping track of all the transform on these
  202. # coordinates: they are relative to the rotations/sheers
  203. # defined in the matrix.
  204. self._x0 = self._x = e
  205. self._y0 = self._y = f
  206. def moveCursor(self, dx, dy):
  207. """Starts a new line at an offset dx,dy from the start of the
  208. current line. This does not move the cursor relative to the
  209. current position, and it changes the current offset of every
  210. future line drawn (i.e. if you next do a textLine() call, it
  211. will move the cursor to a position one line lower than the
  212. position specificied in this call. """
  213. # Check if we have a previous move cursor call, and combine
  214. # them if possible.
  215. if self._code and self._code[-1][-3:]==' Td':
  216. L = self._code[-1].split()
  217. if len(L)==3:
  218. del self._code[-1]
  219. else:
  220. self._code[-1] = ''.join(L[:-4])
  221. # Work out the last movement
  222. lastDx = float(L[-3])
  223. lastDy = float(L[-2])
  224. # Combine the two movement
  225. dx += lastDx
  226. dy -= lastDy
  227. # We will soon add the movement to the line origin, so if
  228. # we've already done this for lastDx, lastDy, remove it
  229. # first (so it will be right when added back again).
  230. self._x0 -= lastDx
  231. self._y0 -= lastDy
  232. # Output the move text cursor call.
  233. self._code.append('%s Td' % fp_str(dx, -dy))
  234. # Keep track of the new line offsets and the cursor position
  235. self._x0 += dx
  236. self._y0 += dy
  237. self._x = self._x0
  238. self._y = self._y0
  239. def setXPos(self, dx):
  240. """Starts a new line dx away from the start of the
  241. current line - NOT from the current point! So if
  242. you call it in mid-sentence, watch out."""
  243. self.moveCursor(dx,0)
  244. def getCursor(self):
  245. """Returns current text position relative to the last origin."""
  246. return (self._x, self._y)
  247. def getStartOfLine(self):
  248. """Returns a tuple giving the text position of the start of the
  249. current line."""
  250. return (self._x0, self._y0)
  251. def getX(self):
  252. """Returns current x position relative to the last origin."""
  253. return self._x
  254. def getY(self):
  255. """Returns current y position relative to the last origin."""
  256. return self._y
  257. def _setFont(self, psfontname, size):
  258. """Sets the font and fontSize
  259. Raises a readable exception if an illegal font
  260. is supplied. Font names are case-sensitive! Keeps track
  261. of font anme and size for metrics."""
  262. self._fontname = psfontname
  263. self._fontsize = size
  264. font = pdfmetrics.getFont(self._fontname)
  265. if font._dynamicFont:
  266. self._curSubset = -1
  267. else:
  268. pdffontname = self._canvas._doc.getInternalFontName(psfontname)
  269. self._code.append('%s %s Tf' % (pdffontname, fp_str(size)))
  270. def setFont(self, psfontname, size, leading = None):
  271. """Sets the font. If leading not specified, defaults to 1.2 x
  272. font size. Raises a readable exception if an illegal font
  273. is supplied. Font names are case-sensitive! Keeps track
  274. of font anme and size for metrics."""
  275. self._fontname = psfontname
  276. self._fontsize = size
  277. if leading is None:
  278. leading = size * 1.2
  279. self._leading = leading
  280. font = pdfmetrics.getFont(self._fontname)
  281. if font._dynamicFont:
  282. self._curSubset = -1
  283. else:
  284. pdffontname = self._canvas._doc.getInternalFontName(psfontname)
  285. self._code.append('%s %s Tf %s TL' % (pdffontname, fp_str(size), fp_str(leading)))
  286. def setCharSpace(self, charSpace):
  287. """Adjusts inter-character spacing"""
  288. self._charSpace = charSpace
  289. self._code.append('%s Tc' % fp_str(charSpace))
  290. def setWordSpace(self, wordSpace):
  291. """Adjust inter-word spacing. This can be used
  292. to flush-justify text - you get the width of the
  293. words, and add some space between them."""
  294. self._wordSpace = wordSpace
  295. self._code.append('%s Tw' % fp_str(wordSpace))
  296. def setHorizScale(self, horizScale):
  297. "Stretches text out horizontally"
  298. self._horizScale = 100 + horizScale
  299. self._code.append('%s Tz' % fp_str(horizScale))
  300. def setLeading(self, leading):
  301. "How far to move down at the end of a line."
  302. self._leading = leading
  303. self._code.append('%s TL' % fp_str(leading))
  304. def setTextRenderMode(self, mode):
  305. """Set the text rendering mode.
  306. 0 = Fill text
  307. 1 = Stroke text
  308. 2 = Fill then stroke
  309. 3 = Invisible
  310. 4 = Fill text and add to clipping path
  311. 5 = Stroke text and add to clipping path
  312. 6 = Fill then stroke and add to clipping path
  313. 7 = Add to clipping path
  314. after we start clipping we mustn't change the mode back until after the ET
  315. """
  316. assert mode in (0,1,2,3,4,5,6,7), "mode must be in (0,1,2,3,4,5,6,7)"
  317. if (mode & 4)!=self._clipping:
  318. mode |= 4
  319. self._clipping = mode & 4
  320. if self._textRenderMode!=mode:
  321. self._textRenderMode = mode
  322. self._code.append('%d Tr' % mode)
  323. def setRise(self, rise):
  324. "Move text baseline up or down to allow superscript/subscripts"
  325. self._rise = rise
  326. self._y = self._y - rise # + ? _textLineMatrix?
  327. self._code.append('%s Ts' % fp_str(rise))
  328. def _formatText(self, text):
  329. "Generates PDF text output operator(s)"
  330. if log2vis and self.direction in ('LTR','RTL'):
  331. # Use pyfribidi to write the text in the correct visual order.
  332. text = log2vis(text, directionsMap.get(self.direction,DIR_ON),clean=True)
  333. canv = self._canvas
  334. font = pdfmetrics.getFont(self._fontname)
  335. R = []
  336. if font._dynamicFont:
  337. #it's a truetype font and should be utf8. If an error is raised,
  338. for subset, t in font.splitString(text, canv._doc):
  339. if subset!=self._curSubset:
  340. pdffontname = font.getSubsetInternalName(subset, canv._doc)
  341. R.append("%s %s Tf %s TL" % (pdffontname, fp_str(self._fontsize), fp_str(self._leading)))
  342. self._curSubset = subset
  343. R.append("(%s) Tj" % canv._escape(t))
  344. elif font._multiByte:
  345. #all the fonts should really work like this - let them know more about PDF...
  346. R.append("%s %s Tf %s TL" % (
  347. canv._doc.getInternalFontName(font.fontName),
  348. fp_str(self._fontsize),
  349. fp_str(self._leading)
  350. ))
  351. R.append("(%s) Tj" % font.formatForPdf(text))
  352. else:
  353. #convert to T1 coding
  354. fc = font
  355. if isBytes(text):
  356. try:
  357. text = text.decode('utf8')
  358. except UnicodeDecodeError as e:
  359. i,j = e.args[2:4]
  360. 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]),)))
  361. for f, t in pdfmetrics.unicode2T1(text,[font]+font.substitutionFonts):
  362. if f!=fc:
  363. R.append("%s %s Tf %s TL" % (canv._doc.getInternalFontName(f.fontName), fp_str(self._fontsize), fp_str(self._leading)))
  364. fc = f
  365. R.append("(%s) Tj" % canv._escape(t))
  366. if font!=fc:
  367. R.append("%s %s Tf %s TL" % (canv._doc.getInternalFontName(self._fontname), fp_str(self._fontsize), fp_str(self._leading)))
  368. return ' '.join(R)
  369. def _textOut(self, text, TStar=0):
  370. "prints string at current point, ignores text cursor"
  371. self._code.append('%s%s' % (self._formatText(text), (TStar and ' T*' or '')))
  372. def textOut(self, text):
  373. """prints string at current point, text cursor moves across."""
  374. self._x = self._x + self._canvas.stringWidth(text, self._fontname, self._fontsize)
  375. self._code.append(self._formatText(text))
  376. def textLine(self, text=''):
  377. """prints string at current point, text cursor moves down.
  378. Can work with no argument to simply move the cursor down."""
  379. # Update the coordinates of the cursor
  380. self._x = self._x0
  381. if self._canvas.bottomup:
  382. self._y = self._y - self._leading
  383. else:
  384. self._y = self._y + self._leading
  385. # Update the location of the start of the line
  386. # self._x0 is unchanged
  387. self._y0 = self._y
  388. # Output the text followed by a PDF newline command
  389. self._code.append('%s T*' % self._formatText(text))
  390. def textLines(self, stuff, trim=1):
  391. """prints multi-line or newlined strings, moving down. One
  392. comon use is to quote a multi-line block in your Python code;
  393. since this may be indented, by default it trims whitespace
  394. off each line and from the beginning; set trim=0 to preserve
  395. whitespace."""
  396. if isStr(stuff):
  397. lines = asUnicode(stuff).strip().split(u'\n')
  398. if trim==1:
  399. lines = [s.strip() for s in lines]
  400. elif isinstance(stuff,(tuple,list)):
  401. lines = stuff
  402. else:
  403. assert 1==0, "argument to textlines must be string,, list or tuple"
  404. # Output each line one at a time. This used to be a long-hand
  405. # copy of the textLine code, now called as a method.
  406. for line in lines:
  407. self.textLine(line)
  408. def __nonzero__(self):
  409. 'PDFTextObject is true if it has something done after the init'
  410. return self._code != ['BT']
  411. def _setFillAlpha(self,v):
  412. self._canvas._doc.ensureMinPdfVersion('transparency')
  413. self._canvas._extgstate.set(self,'ca',v)
  414. def _setStrokeOverprint(self,v):
  415. self._canvas._extgstate.set(self,'OP',v)
  416. def _setFillOverprint(self,v):
  417. self._canvas._extgstate.set(self,'op',v)
  418. def _setOverprintMask(self,v):
  419. self._canvas._extgstate.set(self,'OPM',v and 1 or 0)