362 lines
13 KiB
Python
362 lines
13 KiB
Python
#this is the interface module that imports all from the C extension _rl_accel
|
|
_c_funcs = {}
|
|
_py_funcs = {}
|
|
### NOTE! FP_STR SHOULD PROBABLY ALWAYS DO A PYTHON STR() CONVERSION ON ARGS
|
|
### IN CASE THEY ARE "LAZY OBJECTS". ACCELLERATOR DOESN'T DO THIS (YET)
|
|
__all__ = list(filter(None,'''
|
|
fp_str
|
|
unicode2T1
|
|
instanceStringWidthT1
|
|
instanceStringWidthTTF
|
|
asciiBase85Encode
|
|
asciiBase85Decode
|
|
escapePDF
|
|
sameFrag
|
|
calcChecksum
|
|
add32
|
|
hex32
|
|
'''.split()))
|
|
import reportlab
|
|
testing = getattr(reportlab,'_rl_testing',False)
|
|
del reportlab
|
|
|
|
for fn in __all__:
|
|
D={}
|
|
try:
|
|
exec('from _rl_accel import %s as f' % fn,D)
|
|
_c_funcs[fn] = D['f']
|
|
if testing: _py_funcs[fn] = None
|
|
except ImportError:
|
|
_py_funcs[fn] = None
|
|
del D
|
|
|
|
if _py_funcs:
|
|
from reportlab.lib.utils import isUnicode, isSeq, rawBytes, asNative, asBytes
|
|
from math import log
|
|
from struct import unpack
|
|
|
|
if 'fp_str' in _py_funcs:
|
|
_log_10 = lambda x,log=log,_log_e_10=log(10.0): log(x)/_log_e_10
|
|
_fp_fmts = "%.0f", "%.1f", "%.2f", "%.3f", "%.4f", "%.5f", "%.6f"
|
|
def _py_fp_str(*a):
|
|
'''convert separate arguments (or single sequence arg) into space separated numeric strings'''
|
|
if len(a)==1 and isSeq(a[0]): a = a[0]
|
|
s = []
|
|
A = s.append
|
|
for i in a:
|
|
sa =abs(i)
|
|
if sa<=1e-7: A('0')
|
|
else:
|
|
l = sa<=1 and 6 or min(max(0,(6-int(_log_10(sa)))),6)
|
|
n = _fp_fmts[l]%i
|
|
if l:
|
|
j = len(n)
|
|
while j:
|
|
j -= 1
|
|
if n[j]!='0':
|
|
if n[j]!='.': j += 1
|
|
break
|
|
n = n[:j]
|
|
A((n[0]!='0' or len(n)==1) and n or n[1:])
|
|
return ' '.join(s)
|
|
|
|
#hack test for comma users
|
|
if ',' in _py_fp_str(0.25):
|
|
_FP_STR = _fp_str
|
|
def __py_fp_str(*a):
|
|
return _FP_STR(*a).replace(',','.')
|
|
_py_funcs['fp_str'] = _py_fp_str
|
|
|
|
if 'unicode2T1' in _py_funcs:
|
|
def _py_unicode2T1(utext,fonts):
|
|
'''return a list of (font,string) pairs representing the unicode text'''
|
|
R = []
|
|
font, fonts = fonts[0], fonts[1:]
|
|
enc = font.encName
|
|
if 'UCS-2' in enc:
|
|
enc = 'UTF16'
|
|
while utext:
|
|
try:
|
|
if isUnicode(utext):
|
|
s = utext.encode(enc)
|
|
else:
|
|
s = utext
|
|
R.append((font,s))
|
|
break
|
|
except UnicodeEncodeError as e:
|
|
i0, il = e.args[2:4]
|
|
if i0:
|
|
R.append((font,utext[:i0].encode(enc)))
|
|
if fonts:
|
|
R.extend(_py_unicode2T1(utext[i0:il],fonts))
|
|
else:
|
|
R.append((font._notdefFont,font._notdefChar*(il-i0)))
|
|
utext = utext[il:]
|
|
return R
|
|
_py_funcs['unicode2T1'] = _py_unicode2T1
|
|
|
|
if 'instanceStringWidthT1' in _py_funcs:
|
|
def _py_instanceStringWidthT1(self, text, size, encoding='utf8'):
|
|
"""This is the "purist" approach to width"""
|
|
if not isUnicode(text): text = text.decode(encoding)
|
|
return sum((sum(map(f.widths.__getitem__,t)) for f, t in _py_unicode2T1(text,[self]+self.substitutionFonts)))*0.001*size
|
|
_py_funcs['instanceStringWidthT1'] = _py_instanceStringWidthT1
|
|
|
|
if 'instanceStringWidthTTF' in _py_funcs:
|
|
def _py_instanceStringWidthTTF(self, text, size, encoding='utf8'):
|
|
"Calculate text width"
|
|
if not isUnicode(text):
|
|
text = text.decode(encoding or 'utf8')
|
|
g = self.face.charWidths.get
|
|
dw = self.face.defaultWidth
|
|
return 0.001*size*sum((g(ord(u),dw) for u in text))
|
|
_py_funcs['instanceStringWidthTTF'] = _py_instanceStringWidthTTF
|
|
|
|
if 'hex32' in _py_funcs:
|
|
def _py_hex32(i):
|
|
return '0X%8.8X' % (int(i)&0xFFFFFFFF)
|
|
_py_funcs['hex32'] = _py_hex32
|
|
|
|
if 'add32' in _py_funcs:
|
|
def add32(x, y):
|
|
"Calculate (x + y) modulo 2**32"
|
|
return (x+y) & 0xFFFFFFFF
|
|
_py_funcs['add32'] = add32
|
|
|
|
if 'calcChecksum' in _py_funcs:
|
|
def _py_calcChecksum(data):
|
|
"""Calculates TTF-style checksums"""
|
|
data = rawBytes(data)
|
|
if len(data)&3: data = data + (4-(len(data)&3))*b"\0"
|
|
return sum(unpack(">%dl" % (len(data)>>2), data)) & 0xFFFFFFFF
|
|
_py_funcs['calcChecksum'] = _py_calcChecksum
|
|
|
|
if 'escapePDF' in _py_funcs:
|
|
_ESCAPEDICT={}
|
|
for c in range(256):
|
|
if c<32 or c>=127:
|
|
_ESCAPEDICT[c]= '\\%03o' % c
|
|
elif c in (ord('\\'),ord('('),ord(')')):
|
|
_ESCAPEDICT[c] = '\\'+chr(c)
|
|
else:
|
|
_ESCAPEDICT[c] = chr(c)
|
|
del c
|
|
#Michael Hudson donated this
|
|
def _py_escapePDF(s):
|
|
r = []
|
|
for c in s:
|
|
if not type(c) is int:
|
|
c = ord(c)
|
|
r.append(_ESCAPEDICT[c])
|
|
return ''.join(r)
|
|
_py_funcs['escapePDF'] = _py_escapePDF
|
|
|
|
if 'asciiBase85Encode' in _py_funcs:
|
|
def _py_asciiBase85Encode(input):
|
|
"""Encodes input using ASCII-Base85 coding.
|
|
|
|
This is a compact encoding used for binary data within
|
|
a PDF file. Four bytes of binary data become five bytes of
|
|
ASCII. This is the default method used for encoding images."""
|
|
doOrd = isUnicode(input)
|
|
# special rules apply if not a multiple of four bytes.
|
|
whole_word_count, remainder_size = divmod(len(input), 4)
|
|
cut = 4 * whole_word_count
|
|
body, lastbit = input[0:cut], input[cut:]
|
|
|
|
out = [].append
|
|
for i in range(whole_word_count):
|
|
offset = i*4
|
|
b1 = body[offset]
|
|
b2 = body[offset+1]
|
|
b3 = body[offset+2]
|
|
b4 = body[offset+3]
|
|
if doOrd:
|
|
b1 = ord(b1)
|
|
b2 = ord(b2)
|
|
b3 = ord(b3)
|
|
b4 = ord(b4)
|
|
|
|
if b1<128:
|
|
num = (((((b1<<8)|b2)<<8)|b3)<<8)|b4
|
|
else:
|
|
num = 16777216 * b1 + 65536 * b2 + 256 * b3 + b4
|
|
|
|
if num == 0:
|
|
#special case
|
|
out('z')
|
|
else:
|
|
#solve for five base-85 numbers
|
|
temp, c5 = divmod(num, 85)
|
|
temp, c4 = divmod(temp, 85)
|
|
temp, c3 = divmod(temp, 85)
|
|
c1, c2 = divmod(temp, 85)
|
|
assert ((85**4) * c1) + ((85**3) * c2) + ((85**2) * c3) + (85*c4) + c5 == num, 'dodgy code!'
|
|
out(chr(c1+33))
|
|
out(chr(c2+33))
|
|
out(chr(c3+33))
|
|
out(chr(c4+33))
|
|
out(chr(c5+33))
|
|
|
|
# now we do the final bit at the end. I repeated this separately as
|
|
# the loop above is the time-critical part of a script, whereas this
|
|
# happens only once at the end.
|
|
|
|
#encode however many bytes we have as usual
|
|
if remainder_size > 0:
|
|
lastbit += (4-len(lastbit))*('\0' if doOrd else b'\000')
|
|
b1 = lastbit[0]
|
|
b2 = lastbit[1]
|
|
b3 = lastbit[2]
|
|
b4 = lastbit[3]
|
|
if doOrd:
|
|
b1 = ord(b1)
|
|
b2 = ord(b2)
|
|
b3 = ord(b3)
|
|
b4 = ord(b4)
|
|
|
|
num = 16777216 * b1 + 65536 * b2 + 256 * b3 + b4
|
|
|
|
#solve for c1..c5
|
|
temp, c5 = divmod(num, 85)
|
|
temp, c4 = divmod(temp, 85)
|
|
temp, c3 = divmod(temp, 85)
|
|
c1, c2 = divmod(temp, 85)
|
|
|
|
#print 'encoding: %d %d %d %d -> %d -> %d %d %d %d %d' % (
|
|
# b1,b2,b3,b4,num,c1,c2,c3,c4,c5)
|
|
lastword = chr(c1+33) + chr(c2+33) + chr(c3+33) + chr(c4+33) + chr(c5+33)
|
|
#write out most of the bytes.
|
|
out(lastword[0:remainder_size + 1])
|
|
|
|
#terminator code for ascii 85
|
|
out('~>')
|
|
return ''.join(out.__self__)
|
|
_py_funcs['asciiBase85Encode'] = _py_asciiBase85Encode
|
|
|
|
if 'asciiBase85Decode' in _py_funcs:
|
|
def _py_asciiBase85Decode(input):
|
|
"""Decodes input using ASCII-Base85 coding.
|
|
|
|
This is not normally used - Acrobat Reader decodes for you
|
|
- but a round trip is essential for testing."""
|
|
#strip all whitespace
|
|
stripped = ''.join(asNative(input).split())
|
|
#check end
|
|
assert stripped[-2:] == '~>', 'Invalid terminator for Ascii Base 85 Stream'
|
|
stripped = stripped[:-2] #chop off terminator
|
|
|
|
#may have 'z' in it which complicates matters - expand them
|
|
stripped = stripped.replace('z','!!!!!')
|
|
# special rules apply if not a multiple of five bytes.
|
|
whole_word_count, remainder_size = divmod(len(stripped), 5)
|
|
#print '%d words, %d leftover' % (whole_word_count, remainder_size)
|
|
#assert remainder_size != 1, 'invalid Ascii 85 stream!'
|
|
cut = 5 * whole_word_count
|
|
body, lastbit = stripped[0:cut], stripped[cut:]
|
|
|
|
out = [].append
|
|
for i in range(whole_word_count):
|
|
offset = i*5
|
|
c1 = ord(body[offset]) - 33
|
|
c2 = ord(body[offset+1]) - 33
|
|
c3 = ord(body[offset+2]) - 33
|
|
c4 = ord(body[offset+3]) - 33
|
|
c5 = ord(body[offset+4]) - 33
|
|
|
|
num = ((85**4) * c1) + ((85**3) * c2) + ((85**2) * c3) + (85*c4) + c5
|
|
|
|
temp, b4 = divmod(num,256)
|
|
temp, b3 = divmod(temp,256)
|
|
b1, b2 = divmod(temp, 256)
|
|
|
|
assert num == 16777216 * b1 + 65536 * b2 + 256 * b3 + b4, 'dodgy code!'
|
|
out(chr(b1))
|
|
out(chr(b2))
|
|
out(chr(b3))
|
|
out(chr(b4))
|
|
|
|
#decode however many bytes we have as usual
|
|
if remainder_size > 0:
|
|
while len(lastbit) < 5:
|
|
lastbit = lastbit + '!'
|
|
c1 = ord(lastbit[0]) - 33
|
|
c2 = ord(lastbit[1]) - 33
|
|
c3 = ord(lastbit[2]) - 33
|
|
c4 = ord(lastbit[3]) - 33
|
|
c5 = ord(lastbit[4]) - 33
|
|
num = (((85*c1+c2)*85+c3)*85+c4)*85 + (c5
|
|
+(0,0,0xFFFFFF,0xFFFF,0xFF)[remainder_size])
|
|
temp, b4 = divmod(num,256)
|
|
temp, b3 = divmod(temp,256)
|
|
b1, b2 = divmod(temp, 256)
|
|
assert num == 16777216 * b1 + 65536 * b2 + 256 * b3 + b4, 'dodgy code!'
|
|
#print 'decoding: %d %d %d %d %d -> %d -> %d %d %d %d' % (
|
|
# c1,c2,c3,c4,c5,num,b1,b2,b3,b4)
|
|
|
|
#the last character needs 1 adding; the encoding loses
|
|
#data by rounding the number to x bytes, and when
|
|
#divided repeatedly we get one less
|
|
if remainder_size == 2:
|
|
lastword = chr(b1)
|
|
elif remainder_size == 3:
|
|
lastword = chr(b1) + chr(b2)
|
|
elif remainder_size == 4:
|
|
lastword = chr(b1) + chr(b2) + chr(b3)
|
|
else:
|
|
lastword = ''
|
|
out(lastword)
|
|
|
|
r = ''.join(out.__self__)
|
|
return asBytes(r,enc='latin1')
|
|
_py_funcs['asciiBase85Decode'] = _py_asciiBase85Decode
|
|
|
|
if 'sameFrag' in _py_funcs:
|
|
def _py_sameFrag(f,g, _cmp=('fontName', 'fontSize', 'textColor', 'rise', 'us_lines', 'link', "backColor", "nobr")):
|
|
fdict = f.__dict__
|
|
gdict = g.__dict__
|
|
if 'cbDefn' in fdict or 'lineBreak' in fdict or 'cbDefn' in gdict or 'lineBreak' in gdict: return 0
|
|
fg = fdict.get
|
|
gg = gdict.get
|
|
return [fg(k) for k in _cmp]==[gg(k) for k in _cmp]
|
|
_py_funcs['sameFrag'] = _py_sameFrag
|
|
|
|
G=globals()
|
|
for fn in __all__:
|
|
f = _c_funcs[fn] if fn in _c_funcs else _py_funcs[fn]
|
|
if not f:
|
|
raise RuntimeError('function %s is not properly defined' % fn)
|
|
G[fn] = f
|
|
del fn, f, G
|
|
|
|
if __name__=='__main__':
|
|
import sys, subprocess
|
|
funclist = ','.join("""add32 asciiBase85Decode asciiBase85Encode
|
|
calcChecksum escapePDF fp_str hex32
|
|
instanceStringWidthT1 instanceStringWidthTTF
|
|
sameFrag unicode2T1""".split())
|
|
for cmd,xs in (
|
|
("instanceStringWidthTTF(font,text,10)",("font=TTFont('Vera','Vera.ttf')","text='abcde fghi . jkl ; mno'")),
|
|
("instanceStringWidthT1(font,'abcde fghi . jkl ; mno',10)",
|
|
("fonts=[getFont('Helvetica')]+getFont('Helvetica').substitutionFonts""",
|
|
"font=fonts[0]","text='abcde fghi . jkl ; mno'")),
|
|
("escapePDF(text)",("text='\x11abcdefghijkl\xf3'",)),
|
|
("fp_str(1.23456,2.7891666,2,13,11)",()),
|
|
("calcChecksum(text)",("text=5*' abcdefgiijklMnoPQrstuvwxyz1234567890'",)),
|
|
("hex32(0x12345678)",()),
|
|
("add32(0x12345678,123456789)",()),
|
|
("asciiBase85Encode(src)",("src=5*' abcdefgiijklMnoPQrstuvwxyz1234567890'",)),
|
|
("asciiBase85Decode(_85text)",("_85text=asciiBase85Encode(5*' abcdefgiijklMnoPQrstuvwxyz1234567890')",)),
|
|
):
|
|
for modname in '_rl_accel','reportlab.lib.rl_accel':
|
|
s = ';'.join((
|
|
"from reportlab.pdfbase.pdfmetrics import getFont",
|
|
"from reportlab.pdfbase.ttfonts import TTFont",
|
|
f"from {modname} import {funclist}",
|
|
)+xs)
|
|
if modname!='_rl_accel':
|
|
s = "import sys;sys.modules['_rl_accel']=None;"+s
|
|
print(f'timing {modname} {cmd}')
|
|
for i in range(2):
|
|
subprocess.check_call([sys.executable,'-mtimeit','-s',s,cmd])
|