Initial commit: IT Site Survey AI v2 with all features

This commit is contained in:
BeawIT Admin
2026-07-03 02:06:55 +00:00
commit b4fff79eda
1985 changed files with 437461 additions and 0 deletions
@@ -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'''
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)