Initial commit: IT Site Survey AI v2 with all features
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
#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/__init__.py
|
||||
__version__='3.3.0'
|
||||
__doc__='''Defines a high-level Canvas interface for creating PDF files'''
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
#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/pathobject.py
|
||||
__version__='3.3.0'
|
||||
__doc__="""
|
||||
PDFPathObject is an efficient way to draw paths on 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.pdfgen import pdfgeom
|
||||
from reportlab.lib.rl_accel import fp_str
|
||||
|
||||
|
||||
class PDFPathObject:
|
||||
"""Represents a graphic path. There are certain 'modes' to PDF
|
||||
drawing, and making a separate object to expose Path operations
|
||||
ensures they are completed with no run-time overhead. Ask
|
||||
the Canvas for a PDFPath with getNewPathObject(); moveto/lineto/
|
||||
curveto wherever you want; add whole shapes; and then add it back
|
||||
into the canvas with one of the relevant operators.
|
||||
|
||||
Path objects are probably not long, so we pack onto one line
|
||||
|
||||
the code argument allows a canvas to get the operations appended directly so
|
||||
avoiding the final getCode
|
||||
"""
|
||||
def __init__(self,code=None):
|
||||
self._code = (code,[])[code is None]
|
||||
self._code_append = self._init_code_append
|
||||
|
||||
def _init_code_append(self,c):
|
||||
assert c.endswith(' m') or c.endswith(' re'), 'path must start with a moveto or rect'
|
||||
code_append = self._code.append
|
||||
code_append('n')
|
||||
code_append(c)
|
||||
self._code_append = code_append
|
||||
|
||||
def getCode(self):
|
||||
"pack onto one line; used internally"
|
||||
return ' '.join(self._code)
|
||||
|
||||
def moveTo(self, x, y):
|
||||
self._code_append('%s m' % fp_str(x,y))
|
||||
|
||||
def lineTo(self, x, y):
|
||||
self._code_append('%s l' % fp_str(x,y))
|
||||
|
||||
def curveTo(self, x1, y1, x2, y2, x3, y3):
|
||||
self._code_append('%s c' % fp_str(x1, y1, x2, y2, x3, y3))
|
||||
|
||||
def arc(self, x1,y1, x2,y2, startAng=0, extent=90):
|
||||
"""Contributed to piddlePDF by Robert Kern, 28/7/99.
|
||||
Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2,
|
||||
starting at startAng degrees and covering extent degrees. Angles
|
||||
start with 0 to the right (+x) and increase counter-clockwise.
|
||||
These should have x1<x2 and y1<y2.
|
||||
|
||||
The algorithm is an elliptical generalization of the formulae in
|
||||
Jim Fitzsimmon's TeX tutorial <URL: http://www.tinaja.com/bezarc1.pdf>."""
|
||||
|
||||
self._curves(pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent))
|
||||
|
||||
def arcTo(self, x1,y1, x2,y2, startAng=0, extent=90):
|
||||
"""Like arc, but draws a line from the current point to
|
||||
the start if the start is not the current point."""
|
||||
self._curves(pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent),'lineTo')
|
||||
|
||||
def rect(self, x, y, width, height):
|
||||
"""Adds a rectangle to the path"""
|
||||
self._code_append('%s re' % fp_str((x, y, width, height)))
|
||||
|
||||
def ellipse(self, x, y, width, height):
|
||||
"""adds an ellipse to the path"""
|
||||
self._curves(pdfgeom.bezierArc(x, y, x + width,y + height, 0, 360))
|
||||
|
||||
def _curves(self,curves,initial='moveTo'):
|
||||
getattr(self,initial)(*curves[0][:2])
|
||||
for curve in curves:
|
||||
self.curveTo(*curve[2:])
|
||||
|
||||
def circle(self, x_cen, y_cen, r):
|
||||
"""adds a circle to the path"""
|
||||
x1 = x_cen - r
|
||||
y1 = y_cen - r
|
||||
width = height = 2*r
|
||||
self.ellipse(x1, y1, width, height)
|
||||
|
||||
def roundRect(self, x, y, width, height, radius):
|
||||
"""Draws a rectangle with rounded corners. The corners are
|
||||
approximately quadrants of a circle, with the given radius."""
|
||||
#use a precomputed set of factors for the bezier approximation
|
||||
#to a circle. There are six relevant points on the x axis and y axis.
|
||||
#sketch them and it should all make sense!
|
||||
m = 0.4472 #radius multiplier
|
||||
xhi = x,x+width
|
||||
xlo, xhi = min(xhi), max(xhi)
|
||||
yhi = y,y+height
|
||||
ylo, yhi = min(yhi), max(yhi)
|
||||
if isinstance(radius,(list,tuple)):
|
||||
r = [max(0,r) for r in radius]
|
||||
if len(r)<4: r += (4-len(r))*[0]
|
||||
self.moveTo(xlo + r[2], ylo) #start at bottom left
|
||||
self.lineTo(xhi - r[3], ylo) #bottom row
|
||||
if r[3]>0:
|
||||
t = m*r[3]
|
||||
self.curveTo(xhi - t, ylo, xhi, ylo + t, xhi, ylo + r[3]) #bottom right
|
||||
self.lineTo(xhi, yhi - r[1]) #right edge
|
||||
if r[1]>0:
|
||||
t = m*r[1]
|
||||
self.curveTo(xhi, yhi - t, xhi - t, yhi, xhi - r[1], yhi) #top right
|
||||
self.lineTo(xlo + r[0], yhi) #top row
|
||||
if r[0]>0:
|
||||
t = m*r[0]
|
||||
self.curveTo(xlo + t, yhi, xlo, yhi - t, xlo, yhi - r[0]) #top left
|
||||
self.lineTo(xlo, ylo + r[2]) #left edge
|
||||
if r[2]>0:
|
||||
t = m*r[2]
|
||||
self.curveTo(xlo, ylo + t, xlo + t, ylo, xlo + r[2], ylo) #bottom left
|
||||
# 4 radii top left top right bittom left bottom right
|
||||
else:
|
||||
t = m * radius
|
||||
self.moveTo(xlo + radius, ylo)
|
||||
self.lineTo(xhi - radius, ylo) #bottom row
|
||||
self.curveTo(xhi - t, ylo, xhi, ylo + t, xhi, ylo + radius) #bottom right
|
||||
self.lineTo(xhi, yhi - radius) #right edge
|
||||
self.curveTo(xhi, yhi - t, xhi - t, yhi, xhi - radius, yhi) #top right
|
||||
self.lineTo(xlo + radius, yhi) #top row
|
||||
self.curveTo(xlo + t, yhi, xlo, yhi - t, xlo, yhi - radius) #top left
|
||||
self.lineTo(xlo, ylo + radius) #left edge
|
||||
self.curveTo(xlo, ylo + t, xlo + t, ylo, xlo + radius, ylo) #bottom left
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"draws a line back to where it started"
|
||||
self._code_append('h')
|
||||
@@ -0,0 +1,77 @@
|
||||
#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/pdfgeom.py
|
||||
__version__='3.3.0'
|
||||
__doc__="""
|
||||
This module includes any mathematical methods needed for PIDDLE.
|
||||
It should have no dependencies beyond the Python library.
|
||||
|
||||
So far, just Robert Kern's bezierArc.
|
||||
"""
|
||||
|
||||
from math import sin, cos, pi, ceil
|
||||
|
||||
|
||||
def bezierArc(x1,y1, x2,y2, startAng=0, extent=90):
|
||||
"""bezierArc(x1,y1, x2,y2, startAng=0, extent=90) --> List of Bezier
|
||||
curve control points.
|
||||
|
||||
(x1, y1) and (x2, y2) are the corners of the enclosing rectangle. The
|
||||
coordinate system has coordinates that increase to the right and down.
|
||||
Angles, measured in degress, start with 0 to the right (the positive X
|
||||
axis) and increase counter-clockwise. The arc extends from startAng
|
||||
to startAng+extent. I.e. startAng=0 and extent=180 yields an openside-down
|
||||
semi-circle.
|
||||
|
||||
The resulting coordinates are of the form (x1,y1, x2,y2, x3,y3, x4,y4)
|
||||
such that the curve goes from (x1, y1) to (x4, y4) with (x2, y2) and
|
||||
(x3, y3) as their respective Bezier control points."""
|
||||
|
||||
x1,y1, x2,y2 = min(x1,x2), max(y1,y2), max(x1,x2), min(y1,y2)
|
||||
|
||||
if abs(extent) <= 90:
|
||||
arcList = [startAng]
|
||||
fragAngle = float(extent)
|
||||
Nfrag = 1
|
||||
else:
|
||||
arcList = []
|
||||
Nfrag = int(ceil(abs(extent)/90.))
|
||||
fragAngle = float(extent) / Nfrag
|
||||
|
||||
x_cen = (x1+x2)/2.
|
||||
y_cen = (y1+y2)/2.
|
||||
rx = (x2-x1)/2.
|
||||
ry = (y2-y1)/2.
|
||||
halfAng = fragAngle * pi / 360.
|
||||
kappa = abs(4. / 3. * (1. - cos(halfAng)) / sin(halfAng))
|
||||
|
||||
if fragAngle < 0:
|
||||
sign = -1
|
||||
else:
|
||||
sign = 1
|
||||
|
||||
pointList = []
|
||||
|
||||
for i in range(Nfrag):
|
||||
theta0 = (startAng + i*fragAngle) * pi / 180.
|
||||
theta1 = (startAng + (i+1)*fragAngle) *pi / 180.
|
||||
if fragAngle > 0:
|
||||
pointList.append((x_cen + rx * cos(theta0),
|
||||
y_cen - ry * sin(theta0),
|
||||
x_cen + rx * (cos(theta0) - kappa * sin(theta0)),
|
||||
y_cen - ry * (sin(theta0) + kappa * cos(theta0)),
|
||||
x_cen + rx * (cos(theta1) + kappa * sin(theta1)),
|
||||
y_cen - ry * (sin(theta1) - kappa * cos(theta1)),
|
||||
x_cen + rx * cos(theta1),
|
||||
y_cen - ry * sin(theta1)))
|
||||
else:
|
||||
pointList.append((x_cen + rx * cos(theta0),
|
||||
y_cen - ry * sin(theta0),
|
||||
x_cen + rx * (cos(theta0) + kappa * sin(theta0)),
|
||||
y_cen - ry * (sin(theta0) - kappa * cos(theta0)),
|
||||
x_cen + rx * (cos(theta1) - kappa * sin(theta1)),
|
||||
y_cen - ry * (sin(theta1) + kappa * cos(theta1)),
|
||||
x_cen + rx * cos(theta1),
|
||||
y_cen - ry * sin(theta1)))
|
||||
|
||||
return pointList
|
||||
@@ -0,0 +1,215 @@
|
||||
#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/pdfimages.py
|
||||
__version__='3.3.0'
|
||||
__doc__="""
|
||||
Image functionality sliced out of canvas.py for generalization
|
||||
"""
|
||||
|
||||
import os
|
||||
import reportlab
|
||||
from reportlab import rl_config
|
||||
from reportlab.pdfbase import pdfutils
|
||||
from reportlab.pdfbase import pdfdoc
|
||||
from reportlab.lib.utils import isStr
|
||||
from reportlab.lib.rl_accel import fp_str, asciiBase85Encode
|
||||
from reportlab.lib.boxstuff import aspectRatioFix
|
||||
|
||||
|
||||
class PDFImage:
|
||||
"""Wrapper around different "image sources". You can make images
|
||||
from a PIL Image object, a filename (in which case it uses PIL),
|
||||
an image we previously cached (optimisation, hardly used these
|
||||
days) or a JPEG (which PDF supports natively)."""
|
||||
|
||||
def __init__(self, image, x,y, width=None, height=None, caching=0):
|
||||
self.image = image
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.filename = None
|
||||
self.imageCaching = caching
|
||||
# the following facts need to be determined,
|
||||
# whatever the source. Declare what they are
|
||||
# here for clarity.
|
||||
self.colorSpace = 'DeviceRGB'
|
||||
self.bitsPerComponent = 8
|
||||
self.filters = []
|
||||
self.source = None # JPEG or PIL, set later
|
||||
self.getImageData()
|
||||
|
||||
def jpg_imagedata(self):
|
||||
#directly process JPEG files
|
||||
#open file, needs some error handling!!
|
||||
fp = open(self.image, 'rb')
|
||||
try:
|
||||
result = self._jpg_imagedata(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
return result
|
||||
|
||||
def _jpg_imagedata(self,imageFile):
|
||||
info = pdfutils.readJPEGInfo(imageFile)
|
||||
self.source = 'JPEG'
|
||||
imgwidth, imgheight = info[0], info[1]
|
||||
if info[2] == 1:
|
||||
colorSpace = 'DeviceGray'
|
||||
elif info[2] == 3:
|
||||
colorSpace = 'DeviceRGB'
|
||||
else: #maybe should generate an error, is this right for CMYK?
|
||||
colorSpace = 'DeviceCMYK'
|
||||
imageFile.seek(0) #reset file pointer
|
||||
imagedata = []
|
||||
#imagedata.append('BI /Width %d /Height /BitsPerComponent 8 /ColorSpace /%s /Filter [/Filter [ /ASCII85Decode /DCTDecode] ID' % (info[0], info[1], colorSpace))
|
||||
imagedata.append('BI /W %d /H %d /BPC 8 /CS /%s /F [%s/DCT] ID' % (imgwidth, imgheight, colorSpace, rl_config.useA85 and '/A85 ' or ''))
|
||||
#write in blocks of (??) 60 characters per line to a list
|
||||
data = imageFile.read()
|
||||
if rl_config.useA85:
|
||||
data = asciiBase85Encode(data)
|
||||
pdfutils._chunker(data,imagedata)
|
||||
imagedata.append('EI')
|
||||
return (imagedata, imgwidth, imgheight)
|
||||
|
||||
def cache_imagedata(self):
|
||||
image = self.image
|
||||
if not pdfutils.cachedImageExists(image):
|
||||
pdfutils.cacheImageFile(image)
|
||||
|
||||
#now we have one cached, slurp it in
|
||||
cachedname = os.path.splitext(image)[0] + (rl_config.useA85 and '.a85' or '.bin')
|
||||
imagedata = open(cachedname,'rb').readlines()
|
||||
#trim off newlines...
|
||||
imagedata = list(map(str.strip, imagedata))
|
||||
return imagedata
|
||||
|
||||
def PIL_imagedata(self):
|
||||
import zlib
|
||||
image = self.image
|
||||
if image.format=='JPEG':
|
||||
fp=image.fp
|
||||
fp.seek(0)
|
||||
return self._jpg_imagedata(fp)
|
||||
self.source = 'PIL'
|
||||
|
||||
bpc = 8
|
||||
# Use the colorSpace in the image
|
||||
if image.mode == 'CMYK':
|
||||
myimage = image
|
||||
colorSpace = 'DeviceCMYK'
|
||||
bpp = 4
|
||||
elif image.mode == '1':
|
||||
myimage = image
|
||||
colorSpace = 'DeviceGray'
|
||||
bpp = 1
|
||||
bpc = 1
|
||||
elif image.mode == 'L':
|
||||
myimage = image
|
||||
colorSpace = 'DeviceGray'
|
||||
bpp = 1
|
||||
else:
|
||||
myimage = image.convert('RGB')
|
||||
colorSpace = 'RGB'
|
||||
bpp = 3
|
||||
imgwidth, imgheight = myimage.size
|
||||
|
||||
# this describes what is in the image itself
|
||||
# *NB* according to the spec you can only use the short form in inline images
|
||||
imagedata=['BI /W %d /H %d /BPC %d /CS /%s /F [%s/Fl] ID' % (imgwidth, imgheight, bpc, colorSpace, rl_config.useA85 and '/A85 ' or '')]
|
||||
|
||||
#use a flate filter and, optionally, Ascii Base 85 to compress
|
||||
raw = (myimage.tobytes if hasattr(myimage,'tobytes') else myimage.tostring)()
|
||||
rowstride = (imgwidth*bpc*bpp+7)>>3
|
||||
assert len(raw) == rowstride*imgheight, "Wrong amount of data for image"
|
||||
data = zlib.compress(raw) #this bit is very fast...
|
||||
if rl_config.useA85:
|
||||
data = asciiBase85Encode(data) #...sadly this may not be
|
||||
#append in blocks of 60 characters
|
||||
pdfutils._chunker(data,imagedata)
|
||||
imagedata.append('EI')
|
||||
return (imagedata, imgwidth, imgheight)
|
||||
|
||||
def non_jpg_imagedata(self,image):
|
||||
if not self.imageCaching:
|
||||
imagedata = pdfutils.cacheImageFile(image,returnInMemory=1)
|
||||
else:
|
||||
imagedata = self.cache_imagedata()
|
||||
words = imagedata[1].split()
|
||||
imgwidth = int(words[1])
|
||||
imgheight = int(words[3])
|
||||
return imagedata, imgwidth, imgheight
|
||||
|
||||
def getImageData(self,preserveAspectRatio=False):
|
||||
"Gets data, height, width - whatever type of image"
|
||||
image = self.image
|
||||
|
||||
if isStr(image):
|
||||
self.filename = image
|
||||
if os.path.splitext(image)[1] in ['.jpg', '.JPG', '.jpeg', '.JPEG']:
|
||||
try:
|
||||
imagedata, imgwidth, imgheight = self.jpg_imagedata()
|
||||
except:
|
||||
imagedata, imgwidth, imgheight = self.non_jpg_imagedata(image) #try for normal kind of image
|
||||
else:
|
||||
imagedata, imgwidth, imgheight = self.non_jpg_imagedata(image)
|
||||
else:
|
||||
imagedata, imgwidth, imgheight = self.PIL_imagedata()
|
||||
self.imageData = imagedata
|
||||
self.imgwidth = imgwidth
|
||||
self.imgheight = imgheight
|
||||
self.width = self.width or imgwidth
|
||||
self.height = self.height or imgheight
|
||||
|
||||
def drawInlineImage(self, canvas, preserveAspectRatio=False,anchor='sw', anchorAtXY=False,
|
||||
showBoundary=False, extraReturn=None):
|
||||
"""Draw an Image into the specified rectangle. If width and
|
||||
height are omitted, they are calculated from the image size.
|
||||
Also allow file names as well as images. This allows a
|
||||
caching mechanism"""
|
||||
width = self.width
|
||||
height = self.height
|
||||
if width<1e-6 or height<1e-6: return False
|
||||
x,y,self.width,self.height, scaled = aspectRatioFix(preserveAspectRatio,anchor,self.x,self.y,width,height,self.imgwidth,self.imgheight,anchorAtXY)
|
||||
# this says where and how big to draw it
|
||||
if not canvas.bottomup: y = y+height
|
||||
canvas._code.append('q %s 0 0 %s cm' % (fp_str(self.width), fp_str(self.height, x, y)))
|
||||
width = self.width
|
||||
height = self.height
|
||||
# self._code.extend(imagedata) if >=python-1.5.2
|
||||
for line in self.imageData:
|
||||
canvas._code.append(line)
|
||||
canvas._code.append('Q')
|
||||
if showBoundary:
|
||||
canvas.drawBoundary(showBoundary,x,y,width,height)
|
||||
if extraReturn:
|
||||
for k in extraReturn.keys():
|
||||
extraReturn[k] = vars()[k]
|
||||
return True
|
||||
|
||||
def format(self, document):
|
||||
"""Allow it to be used within pdfdoc framework. This only
|
||||
defines how it is stored, not how it is drawn later."""
|
||||
|
||||
dict = pdfdoc.PDFDictionary()
|
||||
dict['Type'] = '/XObject'
|
||||
dict['Subtype'] = '/Image'
|
||||
dict['Width'] = self.width
|
||||
dict['Height'] = self.height
|
||||
dict['BitsPerComponent'] = 8
|
||||
dict['ColorSpace'] = pdfdoc.PDFName(self.colorSpace)
|
||||
content = '\n'.join(self.imageData[3:-1]) + '\n'
|
||||
strm = pdfdoc.PDFStream(dictionary=dict, content=content)
|
||||
return strm.format(document)
|
||||
|
||||
if __name__=='__main__':
|
||||
srcfile = os.path.join(
|
||||
os.path.dirname(reportlab.__file__),
|
||||
'test',
|
||||
'pythonpowered.gif'
|
||||
)
|
||||
assert os.path.isfile(srcfile), 'image not found'
|
||||
pdfdoc.LongFormat = 1
|
||||
img = PDFImage(srcfile, 100, 100)
|
||||
doc = pdfdoc.PDFDocument()
|
||||
print('source=',img.source)
|
||||
print(img.format(doc))
|
||||
@@ -0,0 +1,751 @@
|
||||
#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)
|
||||
Reference in New Issue
Block a user