Initial commit: IT Site Survey AI v2 with all features
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/__init__.py
|
||||
__version__='3.3.0'
|
||||
__doc__='''Framework for reusable object graphics, in PDF or bitmap form'''
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,152 @@
|
||||
#
|
||||
# Copyright (c) 1996-2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. All advertising materials mentioning features or use of this software
|
||||
# must display the following acknowledgement:
|
||||
# This product includes software developed by Tyler C. Sarna.
|
||||
# 4. Neither the name of the author nor the names of contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
__all__ = tuple('''registerWidget getCodes getCodeNames createBarcodeDrawing createBarcodeImageInMemory'''.split())
|
||||
__version__ = '0.9'
|
||||
__doc__='''Popular barcodes available as reusable widgets'''
|
||||
|
||||
_widgets = []
|
||||
def registerWidget(widget):
|
||||
_widgets.append(widget)
|
||||
|
||||
def _reset():
|
||||
_widgets[:] = []
|
||||
from reportlab.graphics.barcode.widgets import BarcodeI2of5, BarcodeCode128, BarcodeStandard93,\
|
||||
BarcodeExtended93, BarcodeStandard39, BarcodeExtended39,\
|
||||
BarcodeMSI, BarcodeCodabar, BarcodeCode11, BarcodeFIM,\
|
||||
BarcodePOSTNET, BarcodeUSPS_4State, BarcodeCode128Auto, BarcodeECC200DataMatrix
|
||||
|
||||
#newer codes will typically get their own module
|
||||
from reportlab.graphics.barcode.eanbc import Ean13BarcodeWidget, Ean8BarcodeWidget, UPCA, Ean5BarcodeWidget, ISBNBarcodeWidget
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
for widget in (BarcodeI2of5,
|
||||
BarcodeCode128,
|
||||
BarcodeCode128Auto,
|
||||
BarcodeStandard93,
|
||||
BarcodeExtended93,
|
||||
BarcodeStandard39,
|
||||
BarcodeExtended39,
|
||||
BarcodeMSI,
|
||||
BarcodeCodabar,
|
||||
BarcodeCode11,
|
||||
BarcodeFIM,
|
||||
BarcodePOSTNET,
|
||||
BarcodeUSPS_4State,
|
||||
Ean13BarcodeWidget,
|
||||
Ean8BarcodeWidget,
|
||||
UPCA,
|
||||
Ean5BarcodeWidget,
|
||||
ISBNBarcodeWidget,
|
||||
QrCodeWidget,
|
||||
BarcodeECC200DataMatrix,
|
||||
):
|
||||
registerWidget(widget)
|
||||
from reportlab.graphics.barcode import dmtx
|
||||
if dmtx.pylibdmtx:
|
||||
registerWidget(dmtx.DataMatrixWidget)
|
||||
|
||||
_reset()
|
||||
from reportlab.rl_config import register_reset
|
||||
register_reset(_reset)
|
||||
|
||||
def getCodes():
|
||||
"""Returns a dict mapping code names to widgets"""
|
||||
#the module exports a dictionary of names to widgets, to make it easy for
|
||||
#apps and doc tools to display information about them.
|
||||
codes = {}
|
||||
for widget in _widgets:
|
||||
codeName = widget.codeName
|
||||
codes[codeName] = widget
|
||||
|
||||
return codes
|
||||
|
||||
def getCodeNames():
|
||||
"""Returns sorted list of supported bar code names"""
|
||||
return sorted(getCodes().keys())
|
||||
|
||||
def createBarcodeDrawing(codeName, **options):
|
||||
"""This creates and returns a drawing with a barcode.
|
||||
"""
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
|
||||
codes = getCodes()
|
||||
bcc = codes[codeName]
|
||||
width = options.pop('width',None)
|
||||
height = options.pop('height',None)
|
||||
isoScale = options.pop('isoScale',0)
|
||||
kw = {}
|
||||
for k,v in options.items():
|
||||
if k.startswith('_') or k in bcc._attrMap: kw[k] = v
|
||||
bc = bcc(**kw)
|
||||
|
||||
|
||||
#Robin's new ones validate when setting the value property.
|
||||
#Ty Sarna's old ones do not. We need to test.
|
||||
if hasattr(bc, 'validate'):
|
||||
bc.validate() #raise exception if bad value
|
||||
if not bc.valid:
|
||||
raise ValueError("Illegal barcode with value '%s' in code '%s'" % (options.get('value',None), codeName))
|
||||
|
||||
#size it after setting the data
|
||||
x1, y1, x2, y2 = bc.getBounds()
|
||||
w = float(x2 - x1)
|
||||
h = float(y2 - y1)
|
||||
sx = width not in ('auto',None)
|
||||
sy = height not in ('auto',None)
|
||||
if sx or sy:
|
||||
sx = sx and width/w or 1.0
|
||||
sy = sy and height/h or 1.0
|
||||
if isoScale:
|
||||
if sx<1.0 and sy<1.0:
|
||||
sx = sy = max(sx,sy)
|
||||
else:
|
||||
sx = sy = min(sx,sy)
|
||||
|
||||
w *= sx
|
||||
h *= sy
|
||||
else:
|
||||
sx = sy = 1
|
||||
|
||||
#bc.x = -sx*x1
|
||||
#bc.y = -sy*y1
|
||||
d = Drawing(width=w,height=h,transform=[sx,0,0,sy,-sx*x1,-sy*y1])
|
||||
d.add(bc, "_bc")
|
||||
return d
|
||||
|
||||
def createBarcodeImageInMemory(codeName,**options):
|
||||
"""This creates and returns barcode as an image in memory.
|
||||
Takes same arguments as createBarcodeDrawing and also an
|
||||
optional format keyword which can be anything acceptable
|
||||
to Drawing.asString eg gif, pdf, tiff, py ......
|
||||
"""
|
||||
format = options.pop('format','png')
|
||||
d = createBarcodeDrawing(codeName, **options)
|
||||
return d.asString(format)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,460 @@
|
||||
#
|
||||
# Copyright (c) 2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. All advertising materials mentioning features or use of this software
|
||||
# must display the following acknowledgement:
|
||||
# This product includes software developed by Tyler C. Sarna.
|
||||
# 4. Neither the name of the author nor the names of contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.utils import asNative
|
||||
from reportlab.graphics.barcode.common import MultiWidthBarcode
|
||||
from string import digits
|
||||
|
||||
_patterns = {
|
||||
0 : 'BaBbBb', 1 : 'BbBaBb', 2 : 'BbBbBa',
|
||||
3 : 'AbAbBc', 4 : 'AbAcBb', 5 : 'AcAbBb',
|
||||
6 : 'AbBbAc', 7 : 'AbBcAb', 8 : 'AcBbAb',
|
||||
9 : 'BbAbAc', 10 : 'BbAcAb', 11 : 'BcAbAb',
|
||||
12 : 'AaBbCb', 13 : 'AbBaCb', 14 : 'AbBbCa',
|
||||
15 : 'AaCbBb', 16 : 'AbCaBb', 17 : 'AbCbBa',
|
||||
18 : 'BbCbAa', 19 : 'BbAaCb', 20 : 'BbAbCa',
|
||||
21 : 'BaCbAb', 22 : 'BbCaAb', 23 : 'CaBaCa',
|
||||
24 : 'CaAbBb', 25 : 'CbAaBb', 26 : 'CbAbBa',
|
||||
27 : 'CaBbAb', 28 : 'CbBaAb', 29 : 'CbBbAa',
|
||||
30 : 'BaBaBc', 31 : 'BaBcBa', 32 : 'BcBaBa',
|
||||
33 : 'AaAcBc', 34 : 'AcAaBc', 35 : 'AcAcBa',
|
||||
36 : 'AaBcAc', 37 : 'AcBaAc', 38 : 'AcBcAa',
|
||||
39 : 'BaAcAc', 40 : 'BcAaAc', 41 : 'BcAcAa',
|
||||
42 : 'AaBaCc', 43 : 'AaBcCa', 44 : 'AcBaCa',
|
||||
45 : 'AaCaBc', 46 : 'AaCcBa', 47 : 'AcCaBa',
|
||||
48 : 'CaCaBa', 49 : 'BaAcCa', 50 : 'BcAaCa',
|
||||
51 : 'BaCaAc', 52 : 'BaCcAa', 53 : 'BaCaCa',
|
||||
54 : 'CaAaBc', 55 : 'CaAcBa', 56 : 'CcAaBa',
|
||||
57 : 'CaBaAc', 58 : 'CaBcAa', 59 : 'CcBaAa',
|
||||
60 : 'CaDaAa', 61 : 'BbAdAa', 62 : 'DcAaAa',
|
||||
63 : 'AaAbBd', 64 : 'AaAdBb', 65 : 'AbAaBd',
|
||||
66 : 'AbAdBa', 67 : 'AdAaBb', 68 : 'AdAbBa',
|
||||
69 : 'AaBbAd', 70 : 'AaBdAb', 71 : 'AbBaAd',
|
||||
72 : 'AbBdAa', 73 : 'AdBaAb', 74 : 'AdBbAa',
|
||||
75 : 'BdAbAa', 76 : 'BbAaAd', 77 : 'DaCaAa',
|
||||
78 : 'BdAaAb', 79 : 'AcDaAa', 80 : 'AaAbDb',
|
||||
81 : 'AbAaDb', 82 : 'AbAbDa', 83 : 'AaDbAb',
|
||||
84 : 'AbDaAb', 85 : 'AbDbAa', 86 : 'DaAbAb',
|
||||
87 : 'DbAaAb', 88 : 'DbAbAa', 89 : 'BaBaDa',
|
||||
90 : 'BaDaBa', 91 : 'DaBaBa', 92 : 'AaAaDc',
|
||||
93 : 'AaAcDa', 94 : 'AcAaDa', 95 : 'AaDaAc',
|
||||
96 : 'AaDcAa', 97 : 'DaAaAc', 98 : 'DaAcAa',
|
||||
99 : 'AaCaDa', 100 : 'AaDaCa', 101 : 'CaAaDa',
|
||||
102 : 'DaAaCa', 103 : 'BaAdAb', 104 : 'BaAbAd',
|
||||
105 : 'BaAbCb', 106 : 'BcCaAaB'
|
||||
}
|
||||
|
||||
starta, startb, startc, stop = 103, 104, 105, 106
|
||||
|
||||
seta = {
|
||||
' ' : 0, '!' : 1, '"' : 2, '#' : 3,
|
||||
'$' : 4, '%' : 5, '&' : 6, '\'' : 7,
|
||||
'(' : 8, ')' : 9, '*' : 10, '+' : 11,
|
||||
',' : 12, '-' : 13, '.' : 14, '/' : 15,
|
||||
'0' : 16, '1' : 17, '2' : 18, '3' : 19,
|
||||
'4' : 20, '5' : 21, '6' : 22, '7' : 23,
|
||||
'8' : 24, '9' : 25, ':' : 26, ';' : 27,
|
||||
'<' : 28, '=' : 29, '>' : 30, '?' : 31,
|
||||
'@' : 32, 'A' : 33, 'B' : 34, 'C' : 35,
|
||||
'D' : 36, 'E' : 37, 'F' : 38, 'G' : 39,
|
||||
'H' : 40, 'I' : 41, 'J' : 42, 'K' : 43,
|
||||
'L' : 44, 'M' : 45, 'N' : 46, 'O' : 47,
|
||||
'P' : 48, 'Q' : 49, 'R' : 50, 'S' : 51,
|
||||
'T' : 52, 'U' : 53, 'V' : 54, 'W' : 55,
|
||||
'X' : 56, 'Y' : 57, 'Z' : 58, '[' : 59,
|
||||
'\\' : 60, ']' : 61, '^' : 62, '_' : 63,
|
||||
'\x00' : 64, '\x01' : 65, '\x02' : 66, '\x03' : 67,
|
||||
'\x04' : 68, '\x05' : 69, '\x06' : 70, '\x07' : 71,
|
||||
'\x08' : 72, '\x09' : 73, '\x0a' : 74, '\x0b' : 75,
|
||||
'\x0c' : 76, '\x0d' : 77, '\x0e' : 78, '\x0f' : 79,
|
||||
'\x10' : 80, '\x11' : 81, '\x12' : 82, '\x13' : 83,
|
||||
'\x14' : 84, '\x15' : 85, '\x16' : 86, '\x17' : 87,
|
||||
'\x18' : 88, '\x19' : 89, '\x1a' : 90, '\x1b' : 91,
|
||||
'\x1c' : 92, '\x1d' : 93, '\x1e' : 94, '\x1f' : 95,
|
||||
'\xf3' : 96, '\xf2' : 97, 'SHIFT' : 98, 'TO_C' : 99,
|
||||
'TO_B' : 100, '\xf4' : 101, '\xf1' : 102
|
||||
}
|
||||
|
||||
setb = {
|
||||
' ' : 0, '!' : 1, '"' : 2, '#' : 3,
|
||||
'$' : 4, '%' : 5, '&' : 6, '\'' : 7,
|
||||
'(' : 8, ')' : 9, '*' : 10, '+' : 11,
|
||||
',' : 12, '-' : 13, '.' : 14, '/' : 15,
|
||||
'0' : 16, '1' : 17, '2' : 18, '3' : 19,
|
||||
'4' : 20, '5' : 21, '6' : 22, '7' : 23,
|
||||
'8' : 24, '9' : 25, ':' : 26, ';' : 27,
|
||||
'<' : 28, '=' : 29, '>' : 30, '?' : 31,
|
||||
'@' : 32, 'A' : 33, 'B' : 34, 'C' : 35,
|
||||
'D' : 36, 'E' : 37, 'F' : 38, 'G' : 39,
|
||||
'H' : 40, 'I' : 41, 'J' : 42, 'K' : 43,
|
||||
'L' : 44, 'M' : 45, 'N' : 46, 'O' : 47,
|
||||
'P' : 48, 'Q' : 49, 'R' : 50, 'S' : 51,
|
||||
'T' : 52, 'U' : 53, 'V' : 54, 'W' : 55,
|
||||
'X' : 56, 'Y' : 57, 'Z' : 58, '[' : 59,
|
||||
'\\' : 60, ']' : 61, '^' : 62, '_' : 63,
|
||||
'`' : 64, 'a' : 65, 'b' : 66, 'c' : 67,
|
||||
'd' : 68, 'e' : 69, 'f' : 70, 'g' : 71,
|
||||
'h' : 72, 'i' : 73, 'j' : 74, 'k' : 75,
|
||||
'l' : 76, 'm' : 77, 'n' : 78, 'o' : 79,
|
||||
'p' : 80, 'q' : 81, 'r' : 82, 's' : 83,
|
||||
't' : 84, 'u' : 85, 'v' : 86, 'w' : 87,
|
||||
'x' : 88, 'y' : 89, 'z' : 90, '{' : 91,
|
||||
'|' : 92, '}' : 93, '~' : 94, '\x7f' : 95,
|
||||
'\xf3' : 96, '\xf2' : 97, 'SHIFT' : 98, 'TO_C' : 99,
|
||||
'\xf4' : 100, 'TO_A' : 101, '\xf1' : 102
|
||||
}
|
||||
|
||||
setc = {
|
||||
'00': 0, '01': 1, '02': 2, '03': 3, '04': 4,
|
||||
'05': 5, '06': 6, '07': 7, '08': 8, '09': 9,
|
||||
'10':10, '11':11, '12':12, '13':13, '14':14,
|
||||
'15':15, '16':16, '17':17, '18':18, '19':19,
|
||||
'20':20, '21':21, '22':22, '23':23, '24':24,
|
||||
'25':25, '26':26, '27':27, '28':28, '29':29,
|
||||
'30':30, '31':31, '32':32, '33':33, '34':34,
|
||||
'35':35, '36':36, '37':37, '38':38, '39':39,
|
||||
'40':40, '41':41, '42':42, '43':43, '44':44,
|
||||
'45':45, '46':46, '47':47, '48':48, '49':49,
|
||||
'50':50, '51':51, '52':52, '53':53, '54':54,
|
||||
'55':55, '56':56, '57':57, '58':58, '59':59,
|
||||
'60':60, '61':61, '62':62, '63':63, '64':64,
|
||||
'65':65, '66':66, '67':67, '68':68, '69':69,
|
||||
'70':70, '71':71, '72':72, '73':73, '74':74,
|
||||
'75':75, '76':76, '77':77, '78':78, '79':79,
|
||||
'80':80, '81':81, '82':82, '83':83, '84':84,
|
||||
'85':85, '86':86, '87':87, '88':88, '89':89,
|
||||
'90':90, '91':91, '92':92, '93':93, '94':94,
|
||||
'95':95, '96':96, '97':97, '98':98, '99':99,
|
||||
|
||||
'TO_B' : 100, 'TO_A' : 101, '\xf1' : 102
|
||||
}
|
||||
|
||||
setmap = {
|
||||
'TO_A' : (seta, setb),
|
||||
'TO_B' : (setb, seta),
|
||||
'TO_C' : (setc, None),
|
||||
'START_A' : (starta, seta, setb),
|
||||
'START_B' : (startb, setb, seta),
|
||||
'START_C' : (startc, setc, None),
|
||||
}
|
||||
cStarts = ('START_B','TO_A','TO_B')
|
||||
tos = list(setmap.keys())
|
||||
|
||||
class Code128(MultiWidthBarcode):
|
||||
"""
|
||||
Code 128 is a very compact symbology that can encode the entire
|
||||
128 character ASCII set, plus 4 special control codes,
|
||||
(FNC1-FNC4, expressed in the input string as \xf1 to \xf4).
|
||||
Code 128 can also encode digits at double density (2 per byte)
|
||||
and has a mandatory checksum. Code 128 is well supported and
|
||||
commonly used -- for example, by UPS for tracking labels.
|
||||
|
||||
Because of these qualities, Code 128 is probably the best choice
|
||||
for a linear symbology today (assuming you have a choice).
|
||||
|
||||
Options that may be passed to constructor:
|
||||
|
||||
value (int, or numeric string. required.):
|
||||
The value to encode.
|
||||
|
||||
barWidth (float, default .0075):
|
||||
X-Dimension, or width of the smallest element
|
||||
Minumum is .0075 inch (7.5 mils).
|
||||
|
||||
barHeight (float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.
|
||||
|
||||
quiet (bool, default 1):
|
||||
Wether to include quiet zones in the symbol.
|
||||
|
||||
lquiet (float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or 10 barWidth
|
||||
|
||||
rquiet (float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
Sources of Information on Code 128:
|
||||
|
||||
http://www.semiconductor.agilent.com/barcode/sg/Misc/code_128.html
|
||||
http://www.adams1.com/pub/russadam/128code.html
|
||||
http://www.barcodeman.com/c128.html
|
||||
|
||||
Official Spec, "ANSI/AIM BC4-1999, ISS" is available for US$45 from
|
||||
http://www.aimglobal.org/aimstore/
|
||||
"""
|
||||
barWidth = inch * 0.0075
|
||||
lquiet = None
|
||||
rquiet = None
|
||||
quiet = 1
|
||||
barHeight = None
|
||||
def __init__(self, value='', **args):
|
||||
value = str(value) if isinstance(value,int) else asNative(value)
|
||||
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if self.quiet:
|
||||
if self.lquiet is None:
|
||||
self.lquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
if self.rquiet is None:
|
||||
self.rquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
else:
|
||||
self.lquiet = self.rquiet = 0.0
|
||||
|
||||
MultiWidthBarcode.__init__(self, value)
|
||||
|
||||
def validate(self):
|
||||
vval = ""
|
||||
self.valid = 1
|
||||
for c in self.value:
|
||||
if ord(c) > 127 and c not in '\xf1\xf2\xf3\xf4':
|
||||
self.valid = 0
|
||||
continue
|
||||
vval = vval + c
|
||||
self.validated = vval
|
||||
return vval
|
||||
|
||||
|
||||
def _try_TO_C(self, l):
|
||||
'''Improved version of old _trailingDigitsToC(self, l) inspired by'''
|
||||
i = 0
|
||||
nl = []
|
||||
while i < len(l):
|
||||
startpos = i
|
||||
rl = []
|
||||
savings = -1 # the TO_C costs one character
|
||||
while i < len(l):
|
||||
if l[i] in cStarts:
|
||||
j = i
|
||||
break
|
||||
elif l[i] == '\xf1':
|
||||
rl.append(l[i])
|
||||
i += 1
|
||||
continue
|
||||
elif l[i] in digits \
|
||||
and l[i+1] in digits:
|
||||
rl.append(l[i] + l[i+1])
|
||||
i += 2
|
||||
savings += 1
|
||||
continue
|
||||
else:
|
||||
if l[i] in digits and l[i+1]=='STOP':
|
||||
rrl = []
|
||||
rsavings = -1 #we need a TO_C
|
||||
k = i
|
||||
while k>startpos:
|
||||
if l[k]=='\xf1':
|
||||
rrl.append(l[i])
|
||||
k -= 1
|
||||
elif l[k] in digits and l[k-1] in digits:
|
||||
rrl.append(l[k-1]+l[k])
|
||||
rsavings += 1
|
||||
k -= 2
|
||||
else:
|
||||
break
|
||||
rrl.reverse()
|
||||
if rsavings>savings+int(savings>=0 and (startpos and nl[-1] in cStarts))-1:
|
||||
nl += l[startpos]
|
||||
startpos += 1
|
||||
rl = rrl
|
||||
del rrl
|
||||
i += 1
|
||||
break
|
||||
ta = not (l[i]=='STOP' or j==i)
|
||||
xs = savings>=0 and (startpos and nl[-1] in cStarts)
|
||||
if savings+int(xs) > int(ta):
|
||||
if xs:
|
||||
toc = nl[-1][:-1]+'C'
|
||||
del nl[-1]
|
||||
else:
|
||||
toc = 'TO_C'
|
||||
nl += [toc]+rl
|
||||
if ta:
|
||||
nl.append('TO'+l[j][-2:])
|
||||
nl.append(l[i])
|
||||
else:
|
||||
nl += l[startpos:i+1]
|
||||
i += 1
|
||||
return nl
|
||||
|
||||
def encode(self):
|
||||
# First, encode using only B
|
||||
s = self.validated
|
||||
l = ['START_B']
|
||||
for c in s:
|
||||
if c not in setb:
|
||||
l = l + ['TO_A', c, 'TO_B']
|
||||
else:
|
||||
l.append(c)
|
||||
l.append('STOP')
|
||||
|
||||
l = self._try_TO_C(l)
|
||||
|
||||
# Finally, replace START_X,TO_Y with START_Y
|
||||
if l[1] in tos:
|
||||
l[:2] = ['START_' + l[1][-1]]
|
||||
|
||||
# print repr(l)
|
||||
|
||||
# encode into numbers
|
||||
start, set, shset = setmap[l[0]]
|
||||
e = [start]
|
||||
|
||||
l = l[1:-1]
|
||||
while l:
|
||||
c = l[0]
|
||||
if c == 'SHIFT':
|
||||
e = e + [set[c], shset[l[1]]]
|
||||
l = l[2:]
|
||||
elif c in tos:
|
||||
e.append(set[c])
|
||||
set, shset = setmap[c]
|
||||
l = l[1:]
|
||||
else:
|
||||
e.append(set[c])
|
||||
l = l[1:]
|
||||
|
||||
c = e[0]
|
||||
for i in range(1, len(e)):
|
||||
c = c + i * e[i]
|
||||
self.encoded = e + [c % 103, stop]
|
||||
return self.encoded
|
||||
|
||||
def decompose(self):
|
||||
self.decomposed = ''.join([_patterns[c] for c in self.encoded])
|
||||
return self.decomposed
|
||||
|
||||
def _humanText(self):
|
||||
return self.value
|
||||
|
||||
class Code128Auto(Code128):
|
||||
'''contributed by https://bitbucket.org/kylemacfarlane/
|
||||
see https://bitbucket.org/rptlab/reportlab/issues/69/implementations-of-code-128-auto-and-data
|
||||
'''
|
||||
def encode(self):
|
||||
s = self.validated
|
||||
|
||||
current_set = None
|
||||
l = []
|
||||
value = list(s)
|
||||
while value:
|
||||
c = value.pop(0)
|
||||
if c in digits and value and value[0] in digits:
|
||||
c += value.pop(0)
|
||||
|
||||
if c in setc:
|
||||
set_ = 'C'
|
||||
elif c in setb:
|
||||
set_ = 'B'
|
||||
else:
|
||||
set_ = 'A'
|
||||
|
||||
if current_set != set_:
|
||||
if current_set:
|
||||
l.append('TO_' + set_)
|
||||
else:
|
||||
l.append('START_' + set_)
|
||||
current_set = set_
|
||||
|
||||
l.append(c)
|
||||
l.append('STOP')
|
||||
|
||||
start, set, shset = setmap[l[0]]
|
||||
e = [start]
|
||||
|
||||
l = l[1:-1]
|
||||
while l:
|
||||
c = l[0]
|
||||
if c == 'SHIFT':
|
||||
e = e + [set[c], shset[l[1]]]
|
||||
l = l[2:]
|
||||
elif c in tos:
|
||||
e.append(set[c])
|
||||
set, shset = setmap[c]
|
||||
l = l[1:]
|
||||
else:
|
||||
e.append(set[c])
|
||||
l = l[1:]
|
||||
|
||||
c = e[0]
|
||||
for i in range(1, len(e)):
|
||||
c = c + i * e[i]
|
||||
self.encoded = e + [c % 103, stop]
|
||||
return self.encoded
|
||||
|
||||
if __name__=='__main__':
|
||||
def main():
|
||||
from reportlab.graphics.barcode.code128 import Code128
|
||||
from reportlab.platypus import Spacer, SimpleDocTemplate
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.platypus.paragraph import Paragraph
|
||||
from reportlab.platypus.flowables import KeepTogether
|
||||
styles = getSampleStyleSheet()
|
||||
styleN = styles['Normal']
|
||||
styleH = styles['Heading1']
|
||||
story = []
|
||||
storyAdd = story.append
|
||||
for s in (
|
||||
'BBBB123456BBB',
|
||||
'BBBB12345BBB',
|
||||
'BBBB1234BBB',
|
||||
'BBBB123BBB',
|
||||
'BBBB12BBB',
|
||||
'BBBB1BBB',
|
||||
'BBBB123456aa',
|
||||
'BBBB1234aa',
|
||||
'BBBB123aa',
|
||||
'BBBB12aa',
|
||||
'BBBB1aa',
|
||||
'BBBB123456',
|
||||
'BBBB12345',
|
||||
'BBBB1234',
|
||||
'BBBB123',
|
||||
'BBBB12',
|
||||
'BBBB1',
|
||||
'\xf11234B',
|
||||
'Ba\xf11234B',
|
||||
'Ba12',
|
||||
'Ba123B',
|
||||
'Ba1234B',
|
||||
'BBBB1234567',
|
||||
'BBBB1234567aa',
|
||||
):
|
||||
storyAdd(KeepTogether([Paragraph('Code 128 %r' % s, styleN),Code128(s)]))
|
||||
storyAdd(Spacer(inch,inch))
|
||||
SimpleDocTemplate('code128-out.pdf').build(story)
|
||||
main()
|
||||
@@ -0,0 +1,244 @@
|
||||
#
|
||||
# Copyright (c) 1996-2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. All advertising materials mentioning features or use of this software
|
||||
# must display the following acknowledgement:
|
||||
# This product includes software developed by Tyler C. Sarna.
|
||||
# 4. Neither the name of the author nor the names of contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.utils import asNative
|
||||
from reportlab.graphics.barcode.common import Barcode
|
||||
from string import ascii_uppercase, ascii_lowercase, digits as string_digits
|
||||
|
||||
_patterns = {
|
||||
'0': ("bsbSBsBsb", 0), '1': ("BsbSbsbsB", 1),
|
||||
'2': ("bsBSbsbsB", 2), '3': ("BsBSbsbsb", 3),
|
||||
'4': ("bsbSBsbsB", 4), '5': ("BsbSBsbsb", 5),
|
||||
'6': ("bsBSBsbsb", 6), '7': ("bsbSbsBsB", 7),
|
||||
'8': ("BsbSbsBsb", 8), '9': ("bsBSbsBsb", 9),
|
||||
'A': ("BsbsbSbsB", 10), 'B': ("bsBsbSbsB", 11),
|
||||
'C': ("BsBsbSbsb", 12), 'D': ("bsbsBSbsB", 13),
|
||||
'E': ("BsbsBSbsb", 14), 'F': ("bsBsBSbsb", 15),
|
||||
'G': ("bsbsbSBsB", 16), 'H': ("BsbsbSBsb", 17),
|
||||
'I': ("bsBsbSBsb", 18), 'J': ("bsbsBSBsb", 19),
|
||||
'K': ("BsbsbsbSB", 20), 'L': ("bsBsbsbSB", 21),
|
||||
'M': ("BsBsbsbSb", 22), 'N': ("bsbsBsbSB", 23),
|
||||
'O': ("BsbsBsbSb", 24), 'P': ("bsBsBsbSb", 25),
|
||||
'Q': ("bsbsbsBSB", 26), 'R': ("BsbsbsBSb", 27),
|
||||
'S': ("bsBsbsBSb", 28), 'T': ("bsbsBsBSb", 29),
|
||||
'U': ("BSbsbsbsB", 30), 'V': ("bSBsbsbsB", 31),
|
||||
'W': ("BSBsbsbsb", 32), 'X': ("bSbsBsbsB", 33),
|
||||
'Y': ("BSbsBsbsb", 34), 'Z': ("bSBsBsbsb", 35),
|
||||
'-': ("bSbsbsBsB", 36), '.': ("BSbsbsBsb", 37),
|
||||
' ': ("bSBsbsBsb", 38), '*': ("bSbsBsBsb", None),
|
||||
'$': ("bSbSbSbsb", 39), '/': ("bSbSbsbSb", 40),
|
||||
'+': ("bSbsbSbSb", 41), '%': ("bsbSbSbSb", 42)
|
||||
}
|
||||
|
||||
_stdchrs = string_digits + ascii_uppercase + "-. $/+%"
|
||||
|
||||
_extended = {
|
||||
'\0': "%U", '\01': "$A", '\02': "$B", '\03': "$C",
|
||||
'\04': "$D", '\05': "$E", '\06': "$F", '\07': "$G",
|
||||
'\010': "$H", '\011': "$I", '\012': "$J", '\013': "$K",
|
||||
'\014': "$L", '\015': "$M", '\016': "$N", '\017': "$O",
|
||||
'\020': "$P", '\021': "$Q", '\022': "$R", '\023': "$S",
|
||||
'\024': "$T", '\025': "$U", '\026': "$V", '\027': "$W",
|
||||
'\030': "$X", '\031': "$Y", '\032': "$Z", '\033': "%A",
|
||||
'\034': "%B", '\035': "%C", '\036': "%D", '\037': "%E",
|
||||
'!': "/A", '"': "/B", '#': "/C", '$': "/D",
|
||||
'%': "/E", '&': "/F", '\'': "/G", '(': "/H",
|
||||
')': "/I", '*': "/J", '+': "/K", ',': "/L",
|
||||
'/': "/O", ':': "/Z", ';': "%F", '<': "%G",
|
||||
'=': "%H", '>': "%I", '?': "%J", '@': "%V",
|
||||
'[': "%K", '\\': "%L", ']': "%M", '^': "%N",
|
||||
'_': "%O", '`': "%W", 'a': "+A", 'b': "+B",
|
||||
'c': "+C", 'd': "+D", 'e': "+E", 'f': "+F",
|
||||
'g': "+G", 'h': "+H", 'i': "+I", 'j': "+J",
|
||||
'k': "+K", 'l': "+L", 'm': "+M", 'n': "+N",
|
||||
'o': "+O", 'p': "+P", 'q': "+Q", 'r': "+R",
|
||||
's': "+S", 't': "+T", 'u': "+U", 'v': "+V",
|
||||
'w': "+W", 'x': "+X", 'y': "+Y", 'z': "+Z",
|
||||
'{': "%P", '|': "%Q", '}': "%R", '~': "%S",
|
||||
'\177': "%T"
|
||||
}
|
||||
|
||||
|
||||
_extchrs = _stdchrs + ascii_lowercase + \
|
||||
"\000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017" + \
|
||||
"\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037" + \
|
||||
"*!'#&\"(),:;<=>?@[\\]^_`{|}~\177"
|
||||
|
||||
def _encode39(value, cksum, stop):
|
||||
v = sum([_patterns[c][1] for c in value]) % 43
|
||||
if cksum:
|
||||
value += _stdchrs[v]
|
||||
if stop: value = '*'+value+'*'
|
||||
return value
|
||||
|
||||
class _Code39Base(Barcode):
|
||||
barWidth = inch * 0.0075
|
||||
lquiet = None
|
||||
rquiet = None
|
||||
quiet = 1
|
||||
gap = None
|
||||
barHeight = None
|
||||
ratio = 2.2
|
||||
checksum = 1
|
||||
bearers = 0.0
|
||||
stop = 1
|
||||
def __init__(self, value = "", **args):
|
||||
value = asNative(value)
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if self.quiet:
|
||||
if self.lquiet is None:
|
||||
self.lquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
self.rquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
else:
|
||||
self.lquiet = self.rquiet = 0.0
|
||||
|
||||
Barcode.__init__(self, value)
|
||||
|
||||
def decompose(self):
|
||||
dval = ""
|
||||
for c in self.encoded:
|
||||
dval = dval + _patterns[c][0] + 'i'
|
||||
self.decomposed = dval[:-1]
|
||||
return self.decomposed
|
||||
|
||||
def _humanText(self):
|
||||
return self.stop and self.encoded[1:-1] or self.encoded
|
||||
|
||||
class Standard39(_Code39Base):
|
||||
"""
|
||||
Options that may be passed to constructor:
|
||||
|
||||
value (int, or numeric string required.):
|
||||
The value to encode.
|
||||
|
||||
barWidth (float, default .0075):
|
||||
X-Dimension, or width of the smallest element
|
||||
Minumum is .0075 inch (7.5 mils).
|
||||
|
||||
ratio (float, default 2.2):
|
||||
The ratio of wide elements to narrow elements.
|
||||
Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the
|
||||
barWidth is greater than 20 mils (.02 inch))
|
||||
|
||||
gap (float or None, default None):
|
||||
width of intercharacter gap. None means "use barWidth".
|
||||
|
||||
barHeight (float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.
|
||||
|
||||
checksum (bool, default 1):
|
||||
Wether to compute and include the check digit
|
||||
|
||||
bearers (float, in units of barWidth. default 0):
|
||||
Height of bearer bars (horizontal bars along the top and
|
||||
bottom of the barcode). Default is 0 (no bearers).
|
||||
|
||||
quiet (bool, default 1):
|
||||
Wether to include quiet zones in the symbol.
|
||||
|
||||
lquiet (float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or .15 times the symbol's
|
||||
length.
|
||||
|
||||
rquiet (float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
stop (bool, default 1):
|
||||
Whether to include start/stop symbols.
|
||||
|
||||
Sources of Information on Code 39:
|
||||
|
||||
http://www.semiconductor.agilent.com/barcode/sg/Misc/code_39.html
|
||||
http://www.adams1.com/pub/russadam/39code.html
|
||||
http://www.barcodeman.com/c39_1.html
|
||||
|
||||
Official Spec, "ANSI/AIM BC1-1995, USS" is available for US$45 from
|
||||
http://www.aimglobal.org/aimstore/
|
||||
"""
|
||||
def validate(self):
|
||||
vval = [].append
|
||||
self.valid = 1
|
||||
for c in self.value:
|
||||
if c in ascii_lowercase:
|
||||
c = c.upper()
|
||||
if c not in _stdchrs:
|
||||
self.valid = 0
|
||||
continue
|
||||
vval(c)
|
||||
self.validated = ''.join(vval.__self__)
|
||||
return self.validated
|
||||
|
||||
def encode(self):
|
||||
self.encoded = _encode39(self.validated, self.checksum, self.stop)
|
||||
return self.encoded
|
||||
|
||||
class Extended39(_Code39Base):
|
||||
"""
|
||||
Extended Code 39 is a convention for encoding additional characters
|
||||
not present in stanmdard Code 39 by using pairs of characters to
|
||||
represent the characters missing in Standard Code 39.
|
||||
|
||||
See Standard39 for arguments.
|
||||
|
||||
Sources of Information on Extended Code 39:
|
||||
|
||||
http://www.semiconductor.agilent.com/barcode/sg/Misc/xcode_39.html
|
||||
http://www.barcodeman.com/c39_ext.html
|
||||
"""
|
||||
def validate(self):
|
||||
vval = ""
|
||||
self.valid = 1
|
||||
for c in self.value:
|
||||
if c not in _extchrs:
|
||||
self.valid = 0
|
||||
continue
|
||||
vval = vval + c
|
||||
self.validated = vval
|
||||
return vval
|
||||
|
||||
def encode(self):
|
||||
self.encoded = ""
|
||||
for c in self.validated:
|
||||
if c in _extended:
|
||||
self.encoded = self.encoded + _extended[c]
|
||||
elif c in _stdchrs:
|
||||
self.encoded = self.encoded + c
|
||||
else:
|
||||
raise ValueError
|
||||
self.encoded = _encode39(self.encoded, self.checksum,self.stop)
|
||||
return self.encoded
|
||||
@@ -0,0 +1,234 @@
|
||||
#
|
||||
# Copyright (c) 2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. All advertising materials mentioning features or use of this software
|
||||
# must display the following acknowledgement:
|
||||
# This product includes software developed by Tyler C. Sarna.
|
||||
# 4. Neither the name of the author nor the names of contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.utils import asNative
|
||||
from reportlab.graphics.barcode.common import MultiWidthBarcode
|
||||
|
||||
_patterns = {
|
||||
'0' : ('AcAaAb', 0), '1' : ('AaAbAc', 1), '2' : ('AaAcAb', 2),
|
||||
'3' : ('AaAdAa', 3), '4' : ('AbAaAc', 4), '5' : ('AbAbAb', 5),
|
||||
'6' : ('AbAcAa', 6), '7' : ('AaAaAd', 7), '8' : ('AcAbAa', 8),
|
||||
'9' : ('AdAaAa', 9), 'A' : ('BaAaAc', 10), 'B' : ('BaAbAb', 11),
|
||||
'C' : ('BaAcAa', 12), 'D' : ('BbAaAb', 13), 'E' : ('BbAbAa', 14),
|
||||
'F' : ('BcAaAa', 15), 'G' : ('AaBaAc', 16), 'H' : ('AaBbAb', 17),
|
||||
'I' : ('AaBcAa', 18), 'J' : ('AbBaAb', 19), 'K' : ('AcBaAa', 20),
|
||||
'L' : ('AaAaBc', 21), 'M' : ('AaAbBb', 22), 'N' : ('AaAcBa', 23),
|
||||
'O' : ('AbAaBb', 24), 'P' : ('AcAaBa', 25), 'Q' : ('BaBaAb', 26),
|
||||
'R' : ('BaBbAa', 27), 'S' : ('BaAaBb', 28), 'T' : ('BaAbBa', 29),
|
||||
'U' : ('BbAaBa', 30), 'V' : ('BbBaAa', 31), 'W' : ('AaBaBb', 32),
|
||||
'X' : ('AaBbBa', 33), 'Y' : ('AbBaBa', 34), 'Z' : ('AbCaAa', 35),
|
||||
'-' : ('AbAaCa', 36), '.' : ('CaAaAb', 37), ' ' : ('CaAbAa', 38),
|
||||
'$' : ('CbAaAa', 39), '/' : ('AaBaCa', 40), '+' : ('AaCaBa', 41),
|
||||
'%' : ('BaAaCa', 42), '#' : ('AbAbBa', 43), '!' : ('CaBaAa', 44),
|
||||
'=' : ('CaAaBa', 45), '&' : ('AbBbAa', 46),
|
||||
'start' : ('AaAaDa', -1), 'stop' : ('AaAaDaA', -2)
|
||||
}
|
||||
|
||||
_charsbyval = {}
|
||||
for k, v in _patterns.items():
|
||||
_charsbyval[v[1]] = k
|
||||
|
||||
_extended = {
|
||||
'\x00' : '!U', '\x01' : '#A', '\x02' : '#B', '\x03' : '#C',
|
||||
'\x04' : '#D', '\x05' : '#E', '\x06' : '#F', '\x07' : '#G',
|
||||
'\x08' : '#H', '\x09' : '#I', '\x0a' : '#J', '\x0b' : '#K',
|
||||
'\x0c' : '#L', '\x0d' : '#M', '\x0e' : '#N', '\x0f' : '#O',
|
||||
'\x10' : '#P', '\x11' : '#Q', '\x12' : '#R', '\x13' : '#S',
|
||||
'\x14' : '#T', '\x15' : '#U', '\x16' : '#V', '\x17' : '#W',
|
||||
'\x18' : '#X', '\x19' : '#Y', '\x1a' : '#Z', '\x1b' : '!A',
|
||||
'\x1c' : '!B', '\x1d' : '!C', '\x1e' : '!D', '\x1f' : '!E',
|
||||
'!' : '=A', '"' : '=B', '#' : '=C', '$' : '=D',
|
||||
'%' : '=E', '&' : '=F', '\'' : '=G', '(' : '=H',
|
||||
')' : '=I', '*' : '=J', '+' : '=K', ',' : '=L',
|
||||
'/' : '=O', ':' : '=Z', ';' : '!F', '<' : '!G',
|
||||
'=' : '!H', '>' : '!I', '?' : '!J', '@' : '!V',
|
||||
'[' : '!K', '\\' : '!L', ']' : '!M', '^' : '!N',
|
||||
'_' : '!O', '`' : '!W', 'a' : '&A', 'b' : '&B',
|
||||
'c' : '&C', 'd' : '&D', 'e' : '&E', 'f' : '&F',
|
||||
'g' : '&G', 'h' : '&H', 'i' : '&I', 'j' : '&J',
|
||||
'k' : '&K', 'l' : '&L', 'm' : '&M', 'n' : '&N',
|
||||
'o' : '&O', 'p' : '&P', 'q' : '&Q', 'r' : '&R',
|
||||
's' : '&S', 't' : '&T', 'u' : '&U', 'v' : '&V',
|
||||
'w' : '&W', 'x' : '&X', 'y' : '&Y', 'z' : '&Z',
|
||||
'{' : '!P', '|' : '!Q', '}' : '!R', '~' : '!S',
|
||||
'\x7f' : '!T'
|
||||
}
|
||||
|
||||
def _encode93(str):
|
||||
s = list(str)
|
||||
s.reverse()
|
||||
|
||||
# compute 'C' checksum
|
||||
i = 0; v = 1; c = 0
|
||||
while i < len(s):
|
||||
c = c + v * _patterns[s[i]][1]
|
||||
i = i + 1; v = v + 1
|
||||
if v > 20:
|
||||
v = 1
|
||||
s.insert(0, _charsbyval[c % 47])
|
||||
|
||||
# compute 'K' checksum
|
||||
i = 0; v = 1; c = 0
|
||||
while i < len(s):
|
||||
c = c + v * _patterns[s[i]][1]
|
||||
i = i + 1; v = v + 1
|
||||
if v > 15:
|
||||
v = 1
|
||||
s.insert(0, _charsbyval[c % 47])
|
||||
|
||||
s.reverse()
|
||||
|
||||
return ''.join(s)
|
||||
|
||||
class _Code93Base(MultiWidthBarcode):
|
||||
barWidth = inch * 0.0075
|
||||
lquiet = None
|
||||
rquiet = None
|
||||
quiet = 1
|
||||
barHeight = None
|
||||
stop = 1
|
||||
def __init__(self, value='', **args):
|
||||
|
||||
if type(value) is type(1):
|
||||
value = asNative(value)
|
||||
|
||||
for (k, v) in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if self.quiet:
|
||||
if self.lquiet is None:
|
||||
self.lquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
self.rquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
else:
|
||||
self.lquiet = self.rquiet = 0.0
|
||||
|
||||
MultiWidthBarcode.__init__(self, value)
|
||||
|
||||
def decompose(self):
|
||||
dval = self.stop and [_patterns['start'][0]] or []
|
||||
dval += [_patterns[c][0] for c in self.encoded]
|
||||
if self.stop: dval.append(_patterns['stop'][0])
|
||||
self.decomposed = ''.join(dval)
|
||||
return self.decomposed
|
||||
|
||||
class Standard93(_Code93Base):
|
||||
"""
|
||||
Code 93 is a Uppercase alphanumeric symbology with some punctuation.
|
||||
See Extended Code 93 for a variant that can represent the entire
|
||||
128 characrter ASCII set.
|
||||
|
||||
Options that may be passed to constructor:
|
||||
|
||||
value (int, or numeric string. required.):
|
||||
The value to encode.
|
||||
|
||||
barWidth (float, default .0075):
|
||||
X-Dimension, or width of the smallest element
|
||||
Minumum is .0075 inch (7.5 mils).
|
||||
|
||||
barHeight (float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.
|
||||
|
||||
quiet (bool, default 1):
|
||||
Wether to include quiet zones in the symbol.
|
||||
|
||||
lquiet (float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or 10 barWidth
|
||||
|
||||
rquiet (float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
stop (bool, default 1):
|
||||
Whether to include start/stop symbols.
|
||||
|
||||
Sources of Information on Code 93:
|
||||
|
||||
http://www.semiconductor.agilent.com/barcode/sg/Misc/code_93.html
|
||||
|
||||
Official Spec, "NSI/AIM BC5-1995, USS" is available for US$45 from
|
||||
http://www.aimglobal.org/aimstore/
|
||||
"""
|
||||
def validate(self):
|
||||
vval = ""
|
||||
self.valid = 1
|
||||
for c in self.value.upper():
|
||||
if c not in _patterns:
|
||||
self.valid = 0
|
||||
continue
|
||||
vval = vval + c
|
||||
self.validated = vval
|
||||
return vval
|
||||
|
||||
def encode(self):
|
||||
self.encoded = _encode93(self.validated)
|
||||
return self.encoded
|
||||
|
||||
|
||||
class Extended93(_Code93Base):
|
||||
"""
|
||||
Extended Code 93 is a convention for encoding the entire 128 character
|
||||
set using pairs of characters to represent the characters missing in
|
||||
Standard Code 93. It is very much like Extended Code 39 in that way.
|
||||
|
||||
See Standard93 for arguments.
|
||||
"""
|
||||
|
||||
def validate(self):
|
||||
vval = []
|
||||
self.valid = 1
|
||||
a = vval.append
|
||||
for c in self.value:
|
||||
if c not in _patterns and c not in _extended:
|
||||
self.valid = 0
|
||||
continue
|
||||
a(c)
|
||||
self.validated = ''.join(vval)
|
||||
return self.validated
|
||||
|
||||
def encode(self):
|
||||
self.encoded = ""
|
||||
for c in self.validated:
|
||||
if c in _patterns:
|
||||
self.encoded = self.encoded + c
|
||||
elif c in _extended:
|
||||
self.encoded = self.encoded + _extended[c]
|
||||
else:
|
||||
raise ValueError
|
||||
self.encoded = _encode93(self.encoded)
|
||||
return self.encoded
|
||||
|
||||
def _humanText(self):
|
||||
return self.validated+self.encoded[-2:]
|
||||
@@ -0,0 +1,775 @@
|
||||
#
|
||||
# Copyright (c) 1996-2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. All advertising materials mentioning features or use of this software
|
||||
# must display the following acknowledgement:
|
||||
# This product includes software developed by Tyler C. Sarna.
|
||||
# 4. Neither the name of the author nor the names of contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
from reportlab.platypus.flowables import Flowable
|
||||
from reportlab.lib.units import inch
|
||||
from string import ascii_lowercase, ascii_uppercase, digits as string_digits
|
||||
|
||||
class Barcode(Flowable):
|
||||
"""Abstract Base for barcodes. Includes implementations of
|
||||
some methods suitable for the more primitive barcode types"""
|
||||
|
||||
fontName = 'Courier'
|
||||
fontSize = 12
|
||||
humanReadable = 0
|
||||
|
||||
def _humanText(self):
|
||||
return self.encoded
|
||||
|
||||
def __init__(self, value='',**kwd):
|
||||
self.value = str(value)
|
||||
|
||||
self._setKeywords(**kwd)
|
||||
if not hasattr(self, 'gap'):
|
||||
self.gap = None
|
||||
|
||||
|
||||
def _calculate(self):
|
||||
self.validate()
|
||||
self.encode()
|
||||
self.decompose()
|
||||
self.computeSize()
|
||||
|
||||
def _setKeywords(self,**kwd):
|
||||
for (k, v) in kwd.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def validate(self):
|
||||
self.valid = 1
|
||||
self.validated = self.value
|
||||
|
||||
def encode(self):
|
||||
self.encoded = self.validated
|
||||
|
||||
def decompose(self):
|
||||
self.decomposed = self.encoded
|
||||
|
||||
def computeSize(self, *args):
|
||||
barWidth = self.barWidth
|
||||
wx = barWidth * self.ratio
|
||||
|
||||
if self.gap == None:
|
||||
self.gap = barWidth
|
||||
|
||||
w = 0.0
|
||||
|
||||
for c in self.decomposed:
|
||||
if c in 'sb':
|
||||
w = w + barWidth
|
||||
elif c in 'SB':
|
||||
w = w + wx
|
||||
else: # 'i'
|
||||
w = w + self.gap
|
||||
|
||||
if self.barHeight is None:
|
||||
self.barHeight = w * 0.15
|
||||
self.barHeight = max(0.25 * inch, self.barHeight)
|
||||
if self.bearers:
|
||||
self.barHeight = self.barHeight + self.bearers * 2.0 * barWidth
|
||||
|
||||
if self.quiet:
|
||||
w += self.lquiet + self.rquiet
|
||||
|
||||
|
||||
self._height = self.barHeight
|
||||
self._width = w
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
self._calculate()
|
||||
return self._width
|
||||
@width.setter
|
||||
def width(self,v):
|
||||
pass
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
self._calculate()
|
||||
return self._height
|
||||
@height.setter
|
||||
def height(self,v):
|
||||
pass
|
||||
|
||||
def draw(self):
|
||||
self._calculate()
|
||||
barWidth = self.barWidth
|
||||
wx = barWidth * self.ratio
|
||||
|
||||
left = self.quiet and self.lquiet or 0
|
||||
b = self.bearers * barWidth
|
||||
bb = b * 0.5
|
||||
tb = self.barHeight - (b * 1.5)
|
||||
|
||||
for c in self.decomposed:
|
||||
if c == 'i':
|
||||
left = left + self.gap
|
||||
elif c == 's':
|
||||
left = left + barWidth
|
||||
elif c == 'S':
|
||||
left = left + wx
|
||||
elif c == 'b':
|
||||
self.rect(left, bb, barWidth, tb)
|
||||
left = left + barWidth
|
||||
elif c == 'B':
|
||||
self.rect(left, bb, wx, tb)
|
||||
left = left + wx
|
||||
|
||||
if self.bearers:
|
||||
if getattr(self,'bearerBox', None):
|
||||
canv = self.canv
|
||||
if hasattr(canv,'_Gadd'):
|
||||
#this is a widget rect takes other arguments
|
||||
canv.rect(bb, bb, self.width, self.barHeight-b,
|
||||
strokeWidth=b, strokeColor=self.barFillColor or self.barStrokeColor, fillColor=None)
|
||||
else:
|
||||
canv.saveState()
|
||||
canv.setLineWidth(b)
|
||||
canv.rect(bb, bb, self.width, self.barHeight-b, stroke=1, fill=0)
|
||||
canv.restoreState()
|
||||
else:
|
||||
w = self._width - (self.lquiet + self.rquiet)
|
||||
self.rect(self.lquiet, 0, w, b)
|
||||
self.rect(self.lquiet, self.barHeight - b, w, b)
|
||||
|
||||
self.drawHumanReadable()
|
||||
|
||||
def drawHumanReadable(self):
|
||||
if self.humanReadable:
|
||||
#we have text
|
||||
from reportlab.pdfbase.pdfmetrics import getAscent, stringWidth
|
||||
s = str(self._humanText())
|
||||
fontSize = self.fontSize
|
||||
fontName = self.fontName
|
||||
w = stringWidth(s,fontName,fontSize)
|
||||
width = self._width
|
||||
if self.quiet:
|
||||
width -= self.lquiet+self.rquiet
|
||||
x = self.lquiet
|
||||
else:
|
||||
x = 0
|
||||
if w>width: fontSize *= width/float(w)
|
||||
y = 1.07*getAscent(fontName)*fontSize/1000.
|
||||
self.annotate(x+width/2.,-y,s,fontName,fontSize)
|
||||
|
||||
def rect(self, x, y, w, h):
|
||||
self.canv.rect(x, y, w, h, stroke=0, fill=1)
|
||||
|
||||
def annotate(self,x,y,text,fontName,fontSize,anchor='middle'):
|
||||
canv = self.canv
|
||||
canv.saveState()
|
||||
canv.setFont(self.fontName,fontSize)
|
||||
if anchor=='middle': func = 'drawCentredString'
|
||||
elif anchor=='end': func = 'drawRightString'
|
||||
else: func = 'drawString'
|
||||
getattr(canv,func)(x,y,text)
|
||||
canv.restoreState()
|
||||
|
||||
def _checkVal(self, name, v, allowed):
|
||||
if v not in allowed:
|
||||
raise ValueError('%s attribute %s is invalid %r\nnot in allowed %r' % (
|
||||
self.__class__.__name__, name, v, allowed))
|
||||
return v
|
||||
|
||||
class MultiWidthBarcode(Barcode):
|
||||
"""Base for variable-bar-width codes like Code93 and Code128"""
|
||||
|
||||
def computeSize(self, *args):
|
||||
barWidth = self.barWidth
|
||||
oa, oA = ord('a') - 1, ord('A') - 1
|
||||
|
||||
w = 0.0
|
||||
|
||||
for c in self.decomposed:
|
||||
oc = ord(c)
|
||||
if c in ascii_lowercase:
|
||||
w = w + barWidth * (oc - oa)
|
||||
elif c in ascii_uppercase:
|
||||
w = w + barWidth * (oc - oA)
|
||||
|
||||
if self.barHeight is None:
|
||||
self.barHeight = w * 0.15
|
||||
self.barHeight = max(0.25 * inch, self.barHeight)
|
||||
|
||||
if self.quiet:
|
||||
w += self.lquiet + self.rquiet
|
||||
|
||||
self._height = self.barHeight
|
||||
self._width = w
|
||||
|
||||
def draw(self):
|
||||
self._calculate()
|
||||
oa, oA = ord('a') - 1, ord('A') - 1
|
||||
barWidth = self.barWidth
|
||||
left = self.quiet and self.lquiet or 0
|
||||
|
||||
for c in self.decomposed:
|
||||
oc = ord(c)
|
||||
if c in ascii_lowercase:
|
||||
left = left + (oc - oa) * barWidth
|
||||
elif c in ascii_uppercase:
|
||||
w = (oc - oA) * barWidth
|
||||
self.rect(left, 0, w, self.barHeight)
|
||||
left += w
|
||||
self.drawHumanReadable()
|
||||
|
||||
class I2of5(Barcode):
|
||||
"""
|
||||
Interleaved 2 of 5 is a numeric-only barcode. It encodes an even
|
||||
number of digits; if an odd number is given, a 0 is prepended.
|
||||
|
||||
Options that may be passed to constructor:
|
||||
|
||||
value (int, or numeric string required.):
|
||||
The value to encode.
|
||||
|
||||
barWidth (float, default .0075):
|
||||
X-Dimension, or width of the smallest element
|
||||
Minumum is .0075 inch (7.5 mils).
|
||||
|
||||
ratio (float, default 2.2):
|
||||
The ratio of wide elements to narrow elements.
|
||||
Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the
|
||||
barWidth is greater than 20 mils (.02 inch))
|
||||
|
||||
gap (float or None, default None):
|
||||
width of intercharacter gap. None means "use barWidth".
|
||||
|
||||
barHeight (float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.
|
||||
|
||||
checksum (bool, default 1):
|
||||
Whether to compute and include the check digit
|
||||
|
||||
bearers (float, in units of barWidth. default 3.0):
|
||||
Height of bearer bars (horizontal bars along the top and
|
||||
bottom of the barcode). Default is 3 x-dimensions.
|
||||
Set to zero for no bearer bars. (Bearer bars help detect
|
||||
misscans, so it is suggested to leave them on).
|
||||
|
||||
bearerBox (bool default False)
|
||||
if true draw a true rectangle of width bearers around the barcode.
|
||||
|
||||
quiet (bool, default 1):
|
||||
Whether to include quiet zones in the symbol.
|
||||
|
||||
lquiet (float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or .15 times the symbol's
|
||||
length.
|
||||
|
||||
rquiet (float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
stop (bool, default 1):
|
||||
Whether to include start/stop symbols.
|
||||
|
||||
Sources of Information on Interleaved 2 of 5:
|
||||
|
||||
http://www.semiconductor.agilent.com/barcode/sg/Misc/i_25.html
|
||||
http://www.adams1.com/pub/russadam/i25code.html
|
||||
|
||||
Official Spec, "ANSI/AIM BC2-1995, USS" is available for US$45 from
|
||||
http://www.aimglobal.org/aimstore/
|
||||
"""
|
||||
|
||||
patterns = {
|
||||
'start' : 'bsbs',
|
||||
'stop' : 'Bsb',
|
||||
|
||||
'B0' : 'bbBBb', 'S0' : 'ssSSs',
|
||||
'B1' : 'BbbbB', 'S1' : 'SsssS',
|
||||
'B2' : 'bBbbB', 'S2' : 'sSssS',
|
||||
'B3' : 'BBbbb', 'S3' : 'SSsss',
|
||||
'B4' : 'bbBbB', 'S4' : 'ssSsS',
|
||||
'B5' : 'BbBbb', 'S5' : 'SsSss',
|
||||
'B6' : 'bBBbb', 'S6' : 'sSSss',
|
||||
'B7' : 'bbbBB', 'S7' : 'sssSS',
|
||||
'B8' : 'BbbBb', 'S8' : 'SssSs',
|
||||
'B9' : 'bBbBb', 'S9' : 'sSsSs'
|
||||
}
|
||||
|
||||
barHeight = None
|
||||
barWidth = inch * 0.0075
|
||||
ratio = 2.2
|
||||
checksum = 1
|
||||
bearers = 3.0
|
||||
bearerBox = False
|
||||
quiet = 1
|
||||
lquiet = None
|
||||
rquiet = None
|
||||
stop = 1
|
||||
|
||||
def __init__(self, value='', **args):
|
||||
|
||||
if type(value) == type(1):
|
||||
value = str(value)
|
||||
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if self.quiet:
|
||||
if self.lquiet is None:
|
||||
self.lquiet = min(inch * 0.25, self.barWidth * 10.0)
|
||||
self.rquiet = min(inch * 0.25, self.barWidth * 10.0)
|
||||
else:
|
||||
self.lquiet = self.rquiet = 0.0
|
||||
|
||||
Barcode.__init__(self, value)
|
||||
|
||||
def validate(self):
|
||||
vval = ""
|
||||
self.valid = 1
|
||||
for c in self.value.strip():
|
||||
if c not in string_digits:
|
||||
self.valid = 0
|
||||
continue
|
||||
vval = vval + c
|
||||
self.validated = vval
|
||||
return vval
|
||||
|
||||
def encode(self):
|
||||
s = self.validated
|
||||
cs = self.checksum
|
||||
c = len(s)
|
||||
|
||||
#ensure len(result)%2 == 0, checksum included
|
||||
if ((c % 2 == 0) and cs) or ((c % 2 == 1) and not cs):
|
||||
s = '0' + s
|
||||
c += 1
|
||||
|
||||
if cs:
|
||||
c = 3*sum([int(s[i]) for i in range(0,c,2)])+sum([int(s[i]) for i in range(1,c,2)])
|
||||
s += str((10 - c) % 10)
|
||||
|
||||
self.encoded = s
|
||||
|
||||
def decompose(self):
|
||||
dval = self.stop and [self.patterns['start']] or []
|
||||
a = dval.append
|
||||
|
||||
for i in range(0, len(self.encoded), 2):
|
||||
b = self.patterns['B' + self.encoded[i]]
|
||||
s = self.patterns['S' + self.encoded[i+1]]
|
||||
|
||||
for i in range(0, len(b)):
|
||||
a(b[i] + s[i])
|
||||
|
||||
if self.stop: a(self.patterns['stop'])
|
||||
self.decomposed = ''.join(dval)
|
||||
return self.decomposed
|
||||
|
||||
class MSI(Barcode):
|
||||
"""
|
||||
MSI is a numeric-only barcode.
|
||||
|
||||
Options that may be passed to constructor:
|
||||
|
||||
value (int, or numeric string required.):
|
||||
The value to encode.
|
||||
|
||||
barWidth (float, default .0075):
|
||||
X-Dimension, or width of the smallest element
|
||||
|
||||
ratio (float, default 2.2):
|
||||
The ratio of wide elements to narrow elements.
|
||||
|
||||
gap (float or None, default None):
|
||||
width of intercharacter gap. None means "use barWidth".
|
||||
|
||||
barHeight (float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.
|
||||
|
||||
checksum (bool, default 1):
|
||||
Wether to compute and include the check digit
|
||||
|
||||
bearers (float, in units of barWidth. default 0):
|
||||
Height of bearer bars (horizontal bars along the top and
|
||||
bottom of the barcode). Default is 0 (no bearers).
|
||||
|
||||
lquiet (float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or 10 barWidths.
|
||||
|
||||
rquiet (float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
stop (bool, default 1):
|
||||
Whether to include start/stop symbols.
|
||||
|
||||
Sources of Information on MSI Bar Code:
|
||||
|
||||
http://www.semiconductor.agilent.com/barcode/sg/Misc/msi_code.html
|
||||
http://www.adams1.com/pub/russadam/plessy.html
|
||||
"""
|
||||
|
||||
patterns = {
|
||||
'start' : 'Bs', 'stop' : 'bSb',
|
||||
|
||||
'0' : 'bSbSbSbS', '1' : 'bSbSbSBs',
|
||||
'2' : 'bSbSBsbS', '3' : 'bSbSBsBs',
|
||||
'4' : 'bSBsbSbS', '5' : 'bSBsbSBs',
|
||||
'6' : 'bSBsBsbS', '7' : 'bSBsBsBs',
|
||||
'8' : 'BsbSbSbS', '9' : 'BsbSbSBs'
|
||||
}
|
||||
|
||||
stop = 1
|
||||
barHeight = None
|
||||
barWidth = inch * 0.0075
|
||||
ratio = 2.2
|
||||
checksum = 1
|
||||
bearers = 0.0
|
||||
quiet = 1
|
||||
lquiet = None
|
||||
rquiet = None
|
||||
|
||||
def __init__(self, value="", **args):
|
||||
|
||||
if type(value) == type(1):
|
||||
value = str(value)
|
||||
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if self.quiet:
|
||||
if self.lquiet is None:
|
||||
self.lquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
self.rquiet = max(inch * 0.25, self.barWidth * 10.0)
|
||||
else:
|
||||
self.lquiet = self.rquiet = 0.0
|
||||
|
||||
Barcode.__init__(self, value)
|
||||
|
||||
def validate(self):
|
||||
vval = ""
|
||||
self.valid = 1
|
||||
for c in self.value.strip():
|
||||
if c not in string_digits:
|
||||
self.valid = 0
|
||||
continue
|
||||
vval = vval + c
|
||||
self.validated = vval
|
||||
return vval
|
||||
|
||||
def encode(self):
|
||||
s = self.validated
|
||||
|
||||
if self.checksum:
|
||||
c = ''
|
||||
for i in range(1, len(s), 2):
|
||||
c = c + s[i]
|
||||
d = str(int(c) * 2)
|
||||
t = 0
|
||||
for c in d:
|
||||
t = t + int(c)
|
||||
for i in range(0, len(s), 2):
|
||||
t = t + int(s[i])
|
||||
c = 10 - (t % 10)
|
||||
|
||||
s = s + str(c)
|
||||
|
||||
self.encoded = s
|
||||
|
||||
def decompose(self):
|
||||
dval = self.stop and [self.patterns['start']] or []
|
||||
dval += [self.patterns[c] for c in self.encoded]
|
||||
if self.stop: dval.append(self.patterns['stop'])
|
||||
self.decomposed = ''.join(dval)
|
||||
return self.decomposed
|
||||
|
||||
class Codabar(Barcode):
|
||||
"""
|
||||
Codabar is a numeric plus some puntuation ("-$:/.+") barcode
|
||||
with four start/stop characters (A, B, C, and D).
|
||||
|
||||
Options that may be passed to constructor:
|
||||
|
||||
value (string required.):
|
||||
The value to encode.
|
||||
|
||||
barWidth (float, default .0065):
|
||||
X-Dimension, or width of the smallest element
|
||||
minimum is 6.5 mils (.0065 inch)
|
||||
|
||||
ratio (float, default 2.0):
|
||||
The ratio of wide elements to narrow elements.
|
||||
|
||||
gap (float or None, default None):
|
||||
width of intercharacter gap. None means "use barWidth".
|
||||
|
||||
barHeight (float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.
|
||||
|
||||
checksum (bool, default 0):
|
||||
Whether to compute and include the check digit
|
||||
|
||||
bearers (float, in units of barWidth. default 0):
|
||||
Height of bearer bars (horizontal bars along the top and
|
||||
bottom of the barcode). Default is 0 (no bearers).
|
||||
|
||||
quiet (bool, default 1):
|
||||
Whether to include quiet zones in the symbol.
|
||||
|
||||
stop (bool, default 1):
|
||||
Whether to include start/stop symbols.
|
||||
|
||||
lquiet (float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or 10 barWidth
|
||||
|
||||
rquiet (float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
Sources of Information on Codabar
|
||||
|
||||
http://www.semiconductor.agilent.com/barcode/sg/Misc/codabar.html
|
||||
http://www.barcodeman.com/codabar.html
|
||||
|
||||
Official Spec, "ANSI/AIM BC3-1995, USS" is available for US$45 from
|
||||
http://www.aimglobal.org/aimstore/
|
||||
"""
|
||||
|
||||
patterns = {
|
||||
'0': 'bsbsbSB', '1': 'bsbsBSb', '2': 'bsbSbsB',
|
||||
'3': 'BSbsbsb', '4': 'bsBsbSb', '5': 'BsbsbSb',
|
||||
'6': 'bSbsbsB', '7': 'bSbsBsb', '8': 'bSBsbsb',
|
||||
'9': 'BsbSbsb', '-': 'bsbSBsb', '$': 'bsBSbsb',
|
||||
':': 'BsbsBsB', '/': 'BsBsbsB', '.': 'BsBsBsb',
|
||||
'+': 'bsBsBsB', 'A': 'bsBSbSb', 'B': 'bSbSbsB',
|
||||
'C': 'bsbSbSB', 'D': 'bsbSBSb'
|
||||
}
|
||||
|
||||
values = {
|
||||
'0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4,
|
||||
'5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9,
|
||||
'-' : 10, '$' : 11, ':' : 12, '/' : 13, '.' : 14,
|
||||
'+' : 15, 'A' : 16, 'B' : 17, 'C' : 18, 'D' : 19
|
||||
}
|
||||
|
||||
chars = string_digits + "-$:/.+"
|
||||
|
||||
stop = 1
|
||||
barHeight = None
|
||||
barWidth = inch * 0.0065
|
||||
ratio = 2.0 # XXX ?
|
||||
checksum = 0
|
||||
bearers = 0.0
|
||||
quiet = 1
|
||||
lquiet = None
|
||||
rquiet = None
|
||||
|
||||
def __init__(self, value='', **args):
|
||||
if type(value) == type(1):
|
||||
value = str(value)
|
||||
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if self.quiet:
|
||||
if self.lquiet is None:
|
||||
self.lquiet = min(inch * 0.25, self.barWidth * 10.0)
|
||||
self.rquiet = min(inch * 0.25, self.barWidth * 10.0)
|
||||
else:
|
||||
self.lquiet = self.rquiet = 0.0
|
||||
|
||||
Barcode.__init__(self, value)
|
||||
|
||||
def validate(self):
|
||||
vval = ""
|
||||
self.valid = 1
|
||||
s = self.value.strip()
|
||||
for i in range(0, len(s)):
|
||||
c = s[i]
|
||||
if c not in self.chars:
|
||||
if ((i != 0) and (i != len(s) - 1)) or (c not in 'ABCD'):
|
||||
self.Valid = 0
|
||||
continue
|
||||
vval = vval + c
|
||||
|
||||
if self.stop:
|
||||
if vval[0] not in 'ABCD':
|
||||
vval = 'A' + vval
|
||||
if vval[-1] not in 'ABCD':
|
||||
vval = vval + vval[0]
|
||||
|
||||
self.validated = vval
|
||||
return vval
|
||||
|
||||
def encode(self):
|
||||
s = self.validated
|
||||
|
||||
if self.checksum:
|
||||
v = sum([self.values[c] for c in s])
|
||||
s += self.chars[v % 16]
|
||||
|
||||
self.encoded = s
|
||||
|
||||
def decompose(self):
|
||||
dval = ''.join([self.patterns[c]+'i' for c in self.encoded])
|
||||
self.decomposed = dval[:-1]
|
||||
return self.decomposed
|
||||
|
||||
class Code11(Barcode):
|
||||
"""
|
||||
Code 11 is an almost-numeric barcode. It encodes the digits 0-9 plus
|
||||
dash ("-"). 11 characters total, hence the name.
|
||||
|
||||
value (int or string required.):
|
||||
The value to encode.
|
||||
|
||||
barWidth (float, default .0075):
|
||||
X-Dimension, or width of the smallest element
|
||||
|
||||
ratio (float, default 2.2):
|
||||
The ratio of wide elements to narrow elements.
|
||||
|
||||
gap (float or None, default None):
|
||||
width of intercharacter gap. None means "use barWidth".
|
||||
|
||||
barHeight (float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.
|
||||
|
||||
checksum (0 none, 1 1-digit, 2 2-digit, -1 auto, default -1):
|
||||
How many checksum digits to include. -1 ("auto") means
|
||||
1 if the number of digits is 10 or less, else 2.
|
||||
|
||||
bearers (float, in units of barWidth. default 0):
|
||||
Height of bearer bars (horizontal bars along the top and
|
||||
bottom of the barcode). Default is 0 (no bearers).
|
||||
|
||||
quiet (bool, default 1):
|
||||
Wether to include quiet zones in the symbol.
|
||||
|
||||
lquiet (float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or 10 barWidth
|
||||
|
||||
rquiet (float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
Sources of Information on Code 11:
|
||||
|
||||
http://www.cwi.nl/people/dik/english/codes/barcodes.html
|
||||
"""
|
||||
|
||||
chars = '0123456789-'
|
||||
|
||||
patterns = {
|
||||
'0' : 'bsbsB', '1' : 'BsbsB', '2' : 'bSbsB',
|
||||
'3' : 'BSbsb', '4' : 'bsBsB', '5' : 'BsBsb',
|
||||
'6' : 'bSBsb', '7' : 'bsbSB', '8' : 'BsbSb',
|
||||
'9' : 'Bsbsb', '-' : 'bsBsb', 'S' : 'bsBSb' # Start/Stop
|
||||
}
|
||||
|
||||
values = {
|
||||
'0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4,
|
||||
'5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9,
|
||||
'-' : 10,
|
||||
}
|
||||
|
||||
stop = 1
|
||||
barHeight = None
|
||||
barWidth = inch * 0.0075
|
||||
ratio = 2.2 # XXX ?
|
||||
checksum = -1 # Auto
|
||||
bearers = 0.0
|
||||
quiet = 1
|
||||
lquiet = None
|
||||
rquiet = None
|
||||
def __init__(self, value='', **args):
|
||||
if type(value) == type(1):
|
||||
value = str(value)
|
||||
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
if self.quiet:
|
||||
if self.lquiet is None:
|
||||
self.lquiet = min(inch * 0.25, self.barWidth * 10.0)
|
||||
self.rquiet = min(inch * 0.25, self.barWidth * 10.0)
|
||||
else:
|
||||
self.lquiet = self.rquiet = 0.0
|
||||
|
||||
Barcode.__init__(self, value)
|
||||
|
||||
def validate(self):
|
||||
vval = ""
|
||||
self.valid = 1
|
||||
s = self.value.strip()
|
||||
for i in range(0, len(s)):
|
||||
c = s[i]
|
||||
if c not in self.chars:
|
||||
self.Valid = 0
|
||||
continue
|
||||
vval = vval + c
|
||||
|
||||
self.validated = vval
|
||||
return vval
|
||||
|
||||
def _addCSD(self,s,m):
|
||||
# compute first checksum
|
||||
i = c = 0
|
||||
v = 1
|
||||
V = self.values
|
||||
while i < len(s):
|
||||
c += v * V[s[-(i+1)]]
|
||||
i += 1
|
||||
v += 1
|
||||
if v==m:
|
||||
v = 1
|
||||
return s+self.chars[c % 11]
|
||||
|
||||
def encode(self):
|
||||
s = self.validated
|
||||
|
||||
tcs = self.checksum
|
||||
if tcs<0:
|
||||
self.checksum = tcs = 1+int(len(s)>10)
|
||||
|
||||
if tcs > 0: s = self._addCSD(s,11)
|
||||
if tcs > 1: s = self._addCSD(s,10)
|
||||
|
||||
self.encoded = self.stop and ('S' + s + 'S') or s
|
||||
|
||||
def decompose(self):
|
||||
self.decomposed = ''.join([(self.patterns[c]+'i') for c in self.encoded])[:-1]
|
||||
return self.decomposed
|
||||
|
||||
def _humanText(self):
|
||||
return self.stop and self.encoded[1:-1] or self.encoded
|
||||
@@ -0,0 +1,273 @@
|
||||
try:
|
||||
from pylibdmtx import pylibdmtx
|
||||
except ImportError:
|
||||
pylibdmtx = None
|
||||
__all__ = ()
|
||||
else:
|
||||
__all__=('DataMatrix',)
|
||||
|
||||
from reportlab.graphics.barcode.common import Barcode
|
||||
from reportlab.lib.utils import asBytes
|
||||
from reportlab.platypus.paraparser import _num as paraparser_num
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from reportlab.lib.validators import isColor, isString, isColorOrNone, isNumber, isBoxAnchor
|
||||
from reportlab.lib.attrmap import AttrMap, AttrMapValue
|
||||
from reportlab.lib.colors import toColor
|
||||
from reportlab.graphics.shapes import Group, Rect
|
||||
|
||||
def _numConv(x):
|
||||
return x if isinstance(x,(int,float)) else paraparser_num(x)
|
||||
|
||||
class _DMTXCheck:
|
||||
@classmethod
|
||||
def pylibdmtx_check(cls):
|
||||
if not pylibdmtx:
|
||||
raise ValueError('The %s class requires package pylibdmtx' % cls.__name__)
|
||||
|
||||
class DataMatrix(Barcode,_DMTXCheck):
|
||||
def __init__(self, value='', **kwds):
|
||||
self.pylibdmtx_check()
|
||||
self._recalc = True
|
||||
self.value = value
|
||||
self.cellSize = kwds.pop('cellSize','5x5')
|
||||
self.size = kwds.pop('size','SquareAuto')
|
||||
self.encoding = kwds.pop('encoding','Ascii')
|
||||
self.anchor = kwds.pop('anchor','sw')
|
||||
self.color = kwds.pop('color',(0,0,0))
|
||||
self.bgColor = kwds.pop('bgColor',None)
|
||||
self.x = kwds.pop('x',0)
|
||||
self.y = kwds.pop('y',0)
|
||||
self.border = kwds.pop('border',5)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self,v):
|
||||
self._value = asBytes(v)
|
||||
self._recalc = True
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self,v):
|
||||
self._size = self._checkVal('size', v, pylibdmtx.ENCODING_SIZE_NAMES)
|
||||
self._recalc = True
|
||||
|
||||
@property
|
||||
def border(self):
|
||||
return self._border
|
||||
|
||||
@border.setter
|
||||
def border(self,v):
|
||||
self._border = _numConv(v)
|
||||
self._recalc = True
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self._x
|
||||
|
||||
@x.setter
|
||||
def x(self,v):
|
||||
self._x = _numConv(v)
|
||||
self._recalc = True
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self._y
|
||||
|
||||
@y.setter
|
||||
def y(self,v):
|
||||
self._y = _numConv(v)
|
||||
self._recalc = True
|
||||
|
||||
@property
|
||||
def cellSize(self):
|
||||
return self._cellSize
|
||||
|
||||
@size.setter
|
||||
def cellSize(self,v):
|
||||
self._cellSize = v
|
||||
self._recalc = True
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return self._encoding
|
||||
|
||||
@encoding.setter
|
||||
def encoding(self,v):
|
||||
self._encoding = self._checkVal('encoding', v, pylibdmtx.ENCODING_SCHEME_NAMES)
|
||||
self._recalc = True
|
||||
|
||||
@property
|
||||
def anchor(self):
|
||||
return self._anchor
|
||||
|
||||
@anchor.setter
|
||||
def anchor(self,v):
|
||||
self._anchor = self._checkVal('anchor', v, ('n','ne','e','se','s','sw','w','nw','c'))
|
||||
self._recalc = True
|
||||
|
||||
def recalc(self):
|
||||
if not self._recalc: return
|
||||
data = self._value
|
||||
size = self._size
|
||||
encoding = self._encoding
|
||||
e = pylibdmtx.encode(data, size=size, scheme=encoding)
|
||||
iW = e.width
|
||||
iH = e.height
|
||||
p = e.pixels
|
||||
iCellSize = 5
|
||||
bpp = 3 #bytes per pixel
|
||||
rowLen = iW*bpp
|
||||
cellLen = iCellSize*bpp
|
||||
assert len(p)//rowLen == iH
|
||||
matrix = list(filter(None,
|
||||
(''.join(
|
||||
(('x' if p[j:j+bpp] != b'\xff\xff\xff' else ' ')
|
||||
for j in range(i,i+rowLen,cellLen))).strip()
|
||||
for i in range(0,iH*rowLen,rowLen*iCellSize))))
|
||||
self._nRows = len(matrix)
|
||||
self._nCols = len(matrix[-1])
|
||||
self._matrix = '\n'.join(matrix)
|
||||
|
||||
cellWidth = self._cellSize
|
||||
if cellWidth:
|
||||
cellWidth = cellWidth.split('x')
|
||||
if len(cellWidth)>2:
|
||||
raise ValueError('cellSize needs to be distance x distance not %r' % self._cellSize)
|
||||
elif len(cellWidth)==2:
|
||||
cellWidth, cellHeight = cellWidth
|
||||
else:
|
||||
cellWidth = cellHeight = cellWidth[0]
|
||||
cellWidth = _numConv(cellWidth)
|
||||
cellHeight = _numConv(cellHeight)
|
||||
else:
|
||||
cellWidth = cellHeight = iCellSize
|
||||
self._cellWidth = cellWidth
|
||||
self._cellHeight = cellHeight
|
||||
self._recalc = False
|
||||
self._bord = max(self.border,cellWidth,cellHeight)
|
||||
self._width = cellWidth*self._nCols + 2*self._bord
|
||||
self._height = cellHeight*self._nRows + 2*self._bord
|
||||
|
||||
@property
|
||||
def matrix(self):
|
||||
self.recalc()
|
||||
return self._matrix
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
self.recalc()
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
self.recalc()
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def cellWidth(self):
|
||||
self.recalc()
|
||||
return self._cellWidth
|
||||
|
||||
@property
|
||||
def cellHeight(self):
|
||||
self.recalc()
|
||||
return self._cellHeight
|
||||
|
||||
def draw(self):
|
||||
self.recalc()
|
||||
canv = self.canv
|
||||
w = self.width
|
||||
h = self.height
|
||||
x = self.x
|
||||
y = self.y
|
||||
b = self._bord
|
||||
|
||||
anchor = self.anchor
|
||||
if anchor in ('nw','n','ne'):
|
||||
y -= h
|
||||
elif anchor in ('c','e','w'):
|
||||
y -= h//2
|
||||
if anchor in ('ne','e','se'):
|
||||
x -= w
|
||||
elif anchor in ('n','c','s'):
|
||||
x -= w//2
|
||||
|
||||
canv.saveState()
|
||||
if self.bgColor:
|
||||
canv.setFillColor(toColor(self.bgColor))
|
||||
canv.rect(x, y-h, w, h, fill=1, stroke=0)
|
||||
canv.setFillColor(toColor(self.color))
|
||||
canv.setStrokeColor(None)
|
||||
|
||||
cellWidth = self.cellWidth
|
||||
cellHeight = self.cellHeight
|
||||
yr = y - b - cellHeight
|
||||
x += b
|
||||
for row in self.matrix.split('\n'):
|
||||
xr = x
|
||||
for c in row:
|
||||
if c=='x':
|
||||
canv.rect(xr, yr, cellWidth, cellHeight, fill=1, stroke=0)
|
||||
xr += cellWidth
|
||||
yr -= cellHeight
|
||||
canv.restoreState()
|
||||
|
||||
|
||||
class DataMatrixWidget(Widget,_DMTXCheck):
|
||||
codeName = "DataMatrix"
|
||||
_attrMap = AttrMap(
|
||||
BASE = Widget,
|
||||
value = AttrMapValue(isString, desc='Datamatrix data'),
|
||||
x = AttrMapValue(isNumber, desc='x-coord'),
|
||||
y = AttrMapValue(isNumber, desc='y-coord'),
|
||||
color = AttrMapValue(isColor, desc='foreground color'),
|
||||
bgColor = AttrMapValue(isColorOrNone, desc='background color'),
|
||||
encoding = AttrMapValue(isString, desc='encoding'),
|
||||
size = AttrMapValue(isString, desc='size'),
|
||||
cellSize = AttrMapValue(isString, desc='cellSize'),
|
||||
anchor = AttrMapValue(isBoxAnchor, desc='anchor pooint for x,y'),
|
||||
)
|
||||
|
||||
_defaults = dict(
|
||||
x = ('0',_numConv),
|
||||
y = ('0',_numConv),
|
||||
color = ('black',toColor),
|
||||
bgColor = (None,lambda _: toColor(_) if _ is not None else _),
|
||||
encoding = ('Ascii',None),
|
||||
size = ('SquareAuto',None),
|
||||
cellSize = ('5x5',None),
|
||||
anchor = ('sw', None),
|
||||
)
|
||||
def __init__(self,value='Hello Cruel World!', **kwds):
|
||||
self.pylibdmtx_check()
|
||||
self.value = value
|
||||
for k,(d,c) in self._defaults.items():
|
||||
v = kwds.pop(k,d)
|
||||
if c: v = c(v)
|
||||
setattr(self,k,v)
|
||||
|
||||
def rect(self, x, y, w, h, fill=1, stroke=0):
|
||||
self._gadd(Rect(x,y,w,h,strokeColor=None,fillColor=self._fillColor))
|
||||
|
||||
def saveState(self,*args,**kwds):
|
||||
pass
|
||||
|
||||
restoreState = setStrokeColor = saveState
|
||||
|
||||
def setFillColor(self,c):
|
||||
self._fillColor = c
|
||||
|
||||
def draw(self):
|
||||
m = DataMatrix(value=self.value,**{k: getattr(self,k) for k in self._defaults})
|
||||
m.canv = self
|
||||
m.y += m.height
|
||||
g = Group()
|
||||
self._gadd = g.add
|
||||
m.draw()
|
||||
return g
|
||||
@@ -0,0 +1,574 @@
|
||||
__all__=(
|
||||
'Ean13BarcodeWidget','isEanString',
|
||||
'Ean8BarcodeWidget', 'UPCA', 'Ean5BarcodeWidget', 'ISBNBarcodeWidget',
|
||||
)
|
||||
from reportlab.graphics.shapes import Group, String, Rect
|
||||
from reportlab.lib import colors
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
from reportlab.lib.validators import isNumber, isColor, isString, Validator, isBoolean, NoneOr
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import asNative
|
||||
|
||||
#work out a list of manufacturer codes....
|
||||
_eanNumberSystems = [
|
||||
('00-13', 'USA & Canada'),
|
||||
('20-29', 'In-Store Functions'),
|
||||
('30-37', 'France'),
|
||||
('40-44', 'Germany'),
|
||||
('45', 'Japan (also 49)'),
|
||||
('46', 'Russian Federation'),
|
||||
('471', 'Taiwan'),
|
||||
('474', 'Estonia'),
|
||||
('475', 'Latvia'),
|
||||
('477', 'Lithuania'),
|
||||
('479', 'Sri Lanka'),
|
||||
('480', 'Philippines'),
|
||||
('482', 'Ukraine'),
|
||||
('484', 'Moldova'),
|
||||
('485', 'Armenia'),
|
||||
('486', 'Georgia'),
|
||||
('487', 'Kazakhstan'),
|
||||
('489', 'Hong Kong'),
|
||||
('49', 'Japan (JAN-13)'),
|
||||
('50', 'United Kingdom'),
|
||||
('520', 'Greece'),
|
||||
('528', 'Lebanon'),
|
||||
('529', 'Cyprus'),
|
||||
('531', 'Macedonia'),
|
||||
('535', 'Malta'),
|
||||
('539', 'Ireland'),
|
||||
('54', 'Belgium & Luxembourg'),
|
||||
('560', 'Portugal'),
|
||||
('569', 'Iceland'),
|
||||
('57', 'Denmark'),
|
||||
('590', 'Poland'),
|
||||
('594', 'Romania'),
|
||||
('599', 'Hungary'),
|
||||
('600-601', 'South Africa'),
|
||||
('609', 'Mauritius'),
|
||||
('611', 'Morocco'),
|
||||
('613', 'Algeria'),
|
||||
('619', 'Tunisia'),
|
||||
('622', 'Egypt'),
|
||||
('625', 'Jordan'),
|
||||
('626', 'Iran'),
|
||||
('64', 'Finland'),
|
||||
('690-692', 'China'),
|
||||
('70', 'Norway'),
|
||||
('729', 'Israel'),
|
||||
('73', 'Sweden'),
|
||||
('740', 'Guatemala'),
|
||||
('741', 'El Salvador'),
|
||||
('742', 'Honduras'),
|
||||
('743', 'Nicaragua'),
|
||||
('744', 'Costa Rica'),
|
||||
('746', 'Dominican Republic'),
|
||||
('750', 'Mexico'),
|
||||
('759', 'Venezuela'),
|
||||
('76', 'Switzerland'),
|
||||
('770', 'Colombia'),
|
||||
('773', 'Uruguay'),
|
||||
('775', 'Peru'),
|
||||
('777', 'Bolivia'),
|
||||
('779', 'Argentina'),
|
||||
('780', 'Chile'),
|
||||
('784', 'Paraguay'),
|
||||
('785', 'Peru'),
|
||||
('786', 'Ecuador'),
|
||||
('789', 'Brazil'),
|
||||
('80-83', 'Italy'),
|
||||
('84', 'Spain'),
|
||||
('850', 'Cuba'),
|
||||
('858', 'Slovakia'),
|
||||
('859', 'Czech Republic'),
|
||||
('860', 'Yugloslavia'),
|
||||
('869', 'Turkey'),
|
||||
('87', 'Netherlands'),
|
||||
('880', 'South Korea'),
|
||||
('885', 'Thailand'),
|
||||
('888', 'Singapore'),
|
||||
('890', 'India'),
|
||||
('893', 'Vietnam'),
|
||||
('899', 'Indonesia'),
|
||||
('90-91', 'Austria'),
|
||||
('93', 'Australia'),
|
||||
('94', 'New Zealand'),
|
||||
('955', 'Malaysia'),
|
||||
('977', 'International Standard Serial Number for Periodicals (ISSN)'),
|
||||
('978', 'International Standard Book Numbering (ISBN)'),
|
||||
('979', 'International Standard Music Number (ISMN)'),
|
||||
('980', 'Refund receipts'),
|
||||
('981-982', 'Common Currency Coupons'),
|
||||
('99', 'Coupons')
|
||||
]
|
||||
|
||||
manufacturerCodes = {}
|
||||
for (k, v) in _eanNumberSystems:
|
||||
words = k.split('-')
|
||||
if len(words)==2:
|
||||
fromCode = int(words[0])
|
||||
toCode = int(words[1])
|
||||
for code in range(fromCode, toCode+1):
|
||||
manufacturerCodes[code] = v
|
||||
else:
|
||||
manufacturerCodes[int(k)] = v
|
||||
|
||||
def nDigits(n):
|
||||
class _ndigits(Validator):
|
||||
def test(self,x):
|
||||
return type(x) is str and len(x)<=n and len([c for c in x if c in "0123456789"])==n
|
||||
return _ndigits()
|
||||
|
||||
class Ean13BarcodeWidget(PlotArea):
|
||||
codeName = "EAN13"
|
||||
_attrMap = AttrMap(BASE=PlotArea,
|
||||
value = AttrMapValue(nDigits(12), desc='the number'),
|
||||
fontName = AttrMapValue(isString, desc='fontName'),
|
||||
fontSize = AttrMapValue(isNumber, desc='font size'),
|
||||
x = AttrMapValue(isNumber, desc='x-coord'),
|
||||
y = AttrMapValue(isNumber, desc='y-coord'),
|
||||
barFillColor = AttrMapValue(isColor, desc='bar color'),
|
||||
barHeight = AttrMapValue(isNumber, desc='Height of bars.'),
|
||||
barWidth = AttrMapValue(isNumber, desc='Width of bars.'),
|
||||
barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'),
|
||||
barStrokeColor = AttrMapValue(isColor, desc='Color of bar borders.'),
|
||||
textColor = AttrMapValue(isColor, desc='human readable text color'),
|
||||
humanReadable = AttrMapValue(isBoolean, desc='if human readable'),
|
||||
quiet = AttrMapValue(isBoolean, desc='if quiet zone to be used'),
|
||||
lquiet = AttrMapValue(isBoolean, desc='left quiet zone length'),
|
||||
rquiet = AttrMapValue(isBoolean, desc='right quiet zone length'),
|
||||
)
|
||||
_digits=12
|
||||
_start_right = 7 #for ean-13 left = [0:7] right=[7:13]
|
||||
_nbars = 113
|
||||
barHeight = 25.93*mm #millimeters
|
||||
barWidth = (37.29/_nbars)*mm
|
||||
humanReadable = 1
|
||||
_0csw = 1
|
||||
_1csw = 3
|
||||
|
||||
#Left Hand Digits.
|
||||
_left = ( ("0001101", "0011001", "0010011", "0111101",
|
||||
"0100011", "0110001", "0101111", "0111011",
|
||||
"0110111", "0001011",
|
||||
), #odd left hand digits
|
||||
("0100111", "0110011", "0011011", "0100001",
|
||||
"0011101", "0111001", "0000101", "0010001",
|
||||
"0001001", "0010111"), #even left hand digits
|
||||
)
|
||||
|
||||
_right = ("1110010", "1100110", "1101100", "1000010",
|
||||
"1011100", "1001110", "1010000", "1000100",
|
||||
"1001000", "1110100")
|
||||
|
||||
quiet = 1
|
||||
rquiet = lquiet = None
|
||||
_tail = "101"
|
||||
_sep = "01010"
|
||||
|
||||
_lhconvert={
|
||||
"0": (0,0,0,0,0,0),
|
||||
"1": (0,0,1,0,1,1),
|
||||
"2": (0,0,1,1,0,1),
|
||||
"3": (0,0,1,1,1,0),
|
||||
"4": (0,1,0,0,1,1),
|
||||
"5": (0,1,1,0,0,1),
|
||||
"6": (0,1,1,1,0,0),
|
||||
"7": (0,1,0,1,0,1),
|
||||
"8": (0,1,0,1,1,0),
|
||||
"9": (0,1,1,0,1,0)
|
||||
}
|
||||
fontSize = 8 #millimeters
|
||||
fontName = 'Helvetica'
|
||||
textColor = barFillColor = colors.black
|
||||
barStrokeColor = None
|
||||
barStrokeWidth = 0
|
||||
x = 0
|
||||
y = 0
|
||||
def __init__(self,value='123456789012',**kw):
|
||||
value = str(value) if isinstance(value,int) else asNative(value)
|
||||
self.value=max(self._digits-len(value),0)*'0'+value[:self._digits]
|
||||
for k, v in kw.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
width = property(lambda self: self.barWidth*(self._nbars-18+self._calc_quiet(self.lquiet)+self._calc_quiet(self.rquiet)))
|
||||
|
||||
def wrap(self,aW,aH):
|
||||
return self.width,self.barHeight
|
||||
|
||||
def _encode_left(self,s,a):
|
||||
cp = self._lhconvert[s[0]] #convert the left hand numbers
|
||||
_left = self._left
|
||||
z = ord('0')
|
||||
for i,c in enumerate(s[1:self._start_right]):
|
||||
a(_left[cp[i]][ord(c)-z])
|
||||
|
||||
def _short_bar(self,i):
|
||||
i += 9 - self._lquiet
|
||||
return self.humanReadable and ((12<i<55) or (57<i<101))
|
||||
|
||||
def _calc_quiet(self,v):
|
||||
if self.quiet:
|
||||
if v is None:
|
||||
v = 9
|
||||
else:
|
||||
x = float(max(v,0))/self.barWidth
|
||||
v = int(x)
|
||||
if v-x>0: v += 1
|
||||
else:
|
||||
v = 0
|
||||
return v
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
gAdd = g.add
|
||||
barWidth = self.barWidth
|
||||
width = self.width
|
||||
barHeight = self.barHeight
|
||||
x = self.x
|
||||
y = self.y
|
||||
gAdd(Rect(x,y,width,barHeight,fillColor=None,strokeColor=None,strokeWidth=0))
|
||||
s = self.value+self._checkdigit(self.value)
|
||||
self._lquiet = lquiet = self._calc_quiet(self.lquiet)
|
||||
rquiet = self._calc_quiet(self.rquiet)
|
||||
b = [lquiet*'0',self._tail] #the signal string
|
||||
a = b.append
|
||||
self._encode_left(s,a)
|
||||
a(self._sep)
|
||||
|
||||
z = ord('0')
|
||||
_right = self._right
|
||||
for c in s[self._start_right:]:
|
||||
a(_right[ord(c)-z])
|
||||
a(self._tail)
|
||||
a(rquiet*'0')
|
||||
|
||||
fontSize = self.fontSize
|
||||
barFillColor = self.barFillColor
|
||||
barStrokeWidth = self.barStrokeWidth
|
||||
barStrokeColor = self.barStrokeColor
|
||||
|
||||
fth = fontSize*1.2
|
||||
b = ''.join(b)
|
||||
|
||||
lrect = None
|
||||
for i,c in enumerate(b):
|
||||
if c=="1":
|
||||
dh = self._short_bar(i) and fth or 0
|
||||
yh = y+dh
|
||||
if lrect and lrect.y==yh:
|
||||
lrect.width += barWidth
|
||||
else:
|
||||
lrect = Rect(x,yh,barWidth,barHeight-dh,fillColor=barFillColor,strokeWidth=barStrokeWidth,strokeColor=barStrokeColor)
|
||||
gAdd(lrect)
|
||||
else:
|
||||
lrect = None
|
||||
x += barWidth
|
||||
|
||||
if self.humanReadable: self._add_human_readable(s,gAdd)
|
||||
return g
|
||||
|
||||
def _add_human_readable(self,s,gAdd):
|
||||
barWidth = self.barWidth
|
||||
fontSize = self.fontSize
|
||||
textColor = self.textColor
|
||||
fontName = self.fontName
|
||||
fth = fontSize*1.2
|
||||
# draw the num below the line.
|
||||
c = s[0]
|
||||
w = stringWidth(c,fontName,fontSize)
|
||||
x = self.x+barWidth*(self._lquiet-8)
|
||||
y = self.y + 0.2*fth
|
||||
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor))
|
||||
x = self.x + (33-9+self._lquiet)*barWidth
|
||||
|
||||
c = s[1:7]
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
|
||||
|
||||
x += 47*barWidth
|
||||
c = s[7:]
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
|
||||
|
||||
def _checkdigit(cls,num):
|
||||
z = ord('0')
|
||||
iSum = cls._0csw*sum([(ord(x)-z) for x in num[::2]]) \
|
||||
+ cls._1csw*sum([(ord(x)-z) for x in num[1::2]])
|
||||
return chr(z+((10-(iSum%10))%10))
|
||||
_checkdigit=classmethod(_checkdigit)
|
||||
|
||||
class Ean8BarcodeWidget(Ean13BarcodeWidget):
|
||||
codeName = "EAN8"
|
||||
_attrMap = AttrMap(BASE=Ean13BarcodeWidget,
|
||||
value = AttrMapValue(nDigits(7), desc='the number'),
|
||||
)
|
||||
_start_right = 4 #for ean-13 left = [0:7] right=[7:13]
|
||||
_nbars = 85
|
||||
_digits=7
|
||||
_0csw = 3
|
||||
_1csw = 1
|
||||
|
||||
def _encode_left(self,s,a):
|
||||
cp = self._lhconvert[s[0]] #convert the left hand numbers
|
||||
_left = self._left[0]
|
||||
z = ord('0')
|
||||
for i,c in enumerate(s[0:self._start_right]):
|
||||
a(_left[ord(c)-z])
|
||||
|
||||
def _short_bar(self,i):
|
||||
i += 9 - self._lquiet
|
||||
return self.humanReadable and ((12<i<41) or (43<i<73))
|
||||
|
||||
def _add_human_readable(self,s,gAdd):
|
||||
barWidth = self.barWidth
|
||||
fontSize = self.fontSize
|
||||
textColor = self.textColor
|
||||
fontName = self.fontName
|
||||
fth = fontSize*1.2
|
||||
# draw the num below the line.
|
||||
y = self.y + 0.2*fth
|
||||
|
||||
x = (26.5-9+self._lquiet)*barWidth
|
||||
|
||||
c = s[0:4]
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
|
||||
|
||||
x = (59.5-9+self._lquiet)*barWidth
|
||||
c = s[4:]
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
|
||||
|
||||
class UPCA(Ean13BarcodeWidget):
|
||||
codeName = "UPCA"
|
||||
_attrMap = AttrMap(BASE=Ean13BarcodeWidget,
|
||||
value = AttrMapValue(nDigits(11), desc='the number'),
|
||||
)
|
||||
_start_right = 6
|
||||
_digits = 11
|
||||
_0csw = 3
|
||||
_1csw = 1
|
||||
_nbars = 1+7*11+2*3+5
|
||||
|
||||
#these methods contributed by Kyle Macfarlane
|
||||
#https://bitbucket.org/kylemacfarlane/
|
||||
def _encode_left(self,s,a):
|
||||
cp = self._lhconvert[s[0]] #convert the left hand numbers
|
||||
_left = self._left[0]
|
||||
z = ord('0')
|
||||
for i,c in enumerate(s[0:self._start_right]):
|
||||
a(_left[ord(c)-z])
|
||||
|
||||
def _short_bar(self,i):
|
||||
i += 9 - self._lquiet
|
||||
return self.humanReadable and ((18<i<55) or (57<i<93))
|
||||
|
||||
def _add_human_readable(self,s,gAdd):
|
||||
barWidth = self.barWidth
|
||||
fontSize = self.fontSize
|
||||
textColor = self.textColor
|
||||
fontName = self.fontName
|
||||
fth = fontSize*1.2
|
||||
# draw the num below the line.
|
||||
c = s[0]
|
||||
w = stringWidth(c,fontName,fontSize)
|
||||
x = self.x+barWidth*(self._lquiet-8)
|
||||
y = self.y + 0.2*fth
|
||||
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor))
|
||||
x = self.x + (38-9+self._lquiet)*barWidth
|
||||
|
||||
c = s[1:6]
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
|
||||
|
||||
x += 36*barWidth
|
||||
c = s[6:11]
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle'))
|
||||
|
||||
x += 32*barWidth
|
||||
c = s[11]
|
||||
gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor))
|
||||
|
||||
class Ean5BarcodeWidget(Ean13BarcodeWidget):
|
||||
"""
|
||||
EAN-5 barcodes can print the human readable price, set:
|
||||
price=True
|
||||
"""
|
||||
codeName = "EAN5"
|
||||
_attrMap = AttrMap(BASE=Ean13BarcodeWidget,
|
||||
price=AttrMapValue(isBoolean,
|
||||
desc='whether to display the price or not'),
|
||||
value=AttrMapValue(nDigits(5), desc='the number'),
|
||||
)
|
||||
_nbars = 48
|
||||
_digits = 5
|
||||
_sep = '01'
|
||||
_tail = '01011'
|
||||
_0csw = 3
|
||||
_1csw = 9
|
||||
|
||||
_lhconvert = {
|
||||
"0": (1, 1, 0, 0, 0),
|
||||
"1": (1, 0, 1, 0, 0),
|
||||
"2": (1, 0, 0, 1, 0),
|
||||
"3": (1, 0, 0, 0, 1),
|
||||
"4": (0, 1, 1, 0, 0),
|
||||
"5": (0, 0, 1, 1, 0),
|
||||
"6": (0, 0, 0, 1, 1),
|
||||
"7": (0, 1, 0, 1, 0),
|
||||
"8": (0, 1, 0, 0, 1),
|
||||
"9": (0, 0, 1, 0, 1)
|
||||
}
|
||||
|
||||
def _checkdigit(cls, num):
|
||||
z = ord('0')
|
||||
iSum = cls._0csw * sum([(ord(x) - z) for x in num[::2]]) \
|
||||
+ cls._1csw * sum([(ord(x) - z) for x in num[1::2]])
|
||||
return chr(z + iSum % 10)
|
||||
|
||||
def _encode_left(self, s, a):
|
||||
check = self._checkdigit(s)
|
||||
cp = self._lhconvert[check]
|
||||
_left = self._left
|
||||
_sep = self._sep
|
||||
z = ord('0')
|
||||
full_code = []
|
||||
for i, c in enumerate(s):
|
||||
full_code.append(_left[cp[i]][ord(c) - z])
|
||||
a(_sep.join(full_code))
|
||||
|
||||
def _short_bar(self, i):
|
||||
i += 9 - self._lquiet
|
||||
return self.humanReadable and ((12 < i < 41) or (43 < i < 73))
|
||||
|
||||
def _add_human_readable(self, s, gAdd):
|
||||
barWidth = self.barWidth
|
||||
fontSize = self.fontSize
|
||||
textColor = self.textColor
|
||||
fontName = self.fontName
|
||||
fth = fontSize * 1.2
|
||||
# draw the num below the line.
|
||||
y = self.y + 0.2 * fth
|
||||
|
||||
x = self.x + (self._nbars + self._lquiet * 2) * barWidth / 2
|
||||
|
||||
gAdd(String(x, y, s, fontName=fontName, fontSize=fontSize,
|
||||
fillColor=textColor, textAnchor='middle'))
|
||||
|
||||
price = getattr(self,'price',None)
|
||||
if price:
|
||||
price = None
|
||||
if s[0] in '3456':
|
||||
price = '$'
|
||||
elif s[0] in '01':
|
||||
price = asNative(b'\xc2\xa3')
|
||||
|
||||
if price is None:
|
||||
return
|
||||
|
||||
price += s[1:3] + '.' + s[3:5]
|
||||
y += self.barHeight
|
||||
gAdd(String(x, y, price, fontName=fontName, fontSize=fontSize,
|
||||
fillColor=textColor, textAnchor='middle'))
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
gAdd = g.add
|
||||
barWidth = self.barWidth
|
||||
width = self.width
|
||||
barHeight = self.barHeight
|
||||
x = self.x
|
||||
y = self.y
|
||||
gAdd(Rect(x, y, width, barHeight, fillColor=None, strokeColor=None,
|
||||
strokeWidth=0))
|
||||
s = self.value
|
||||
self._lquiet = lquiet = self._calc_quiet(self.lquiet)
|
||||
rquiet = self._calc_quiet(self.rquiet)
|
||||
b = [lquiet * '0' + self._tail] # the signal string
|
||||
a = b.append
|
||||
self._encode_left(s, a)
|
||||
|
||||
a(rquiet * '0')
|
||||
|
||||
fontSize = self.fontSize
|
||||
barFillColor = self.barFillColor
|
||||
barStrokeWidth = self.barStrokeWidth
|
||||
barStrokeColor = self.barStrokeColor
|
||||
|
||||
fth = fontSize * 1.2
|
||||
b = ''.join(b)
|
||||
|
||||
lrect = None
|
||||
for i, c in enumerate(b):
|
||||
if c == "1":
|
||||
dh = fth
|
||||
yh = y + dh
|
||||
if lrect and lrect.y == yh:
|
||||
lrect.width += barWidth
|
||||
else:
|
||||
lrect = Rect(x, yh, barWidth, barHeight - dh,
|
||||
fillColor=barFillColor,
|
||||
strokeWidth=barStrokeWidth,
|
||||
strokeColor=barStrokeColor)
|
||||
gAdd(lrect)
|
||||
else:
|
||||
lrect = None
|
||||
x += barWidth
|
||||
|
||||
if self.humanReadable:
|
||||
self._add_human_readable(s, gAdd)
|
||||
return g
|
||||
|
||||
class ISBNBarcodeWidget(Ean13BarcodeWidget):
|
||||
"""
|
||||
ISBN Barcodes optionally print the EAN-5 supplemental price
|
||||
barcode (with the price in dollars or pounds). Set price to a string
|
||||
that follows the EAN-5 for ISBN spec:
|
||||
|
||||
leading digit 0, 1 = GBP
|
||||
3 = AUD
|
||||
4 = NZD
|
||||
5 = USD
|
||||
6 = CAD
|
||||
next 4 digits = price between 00.00 and 99.98, i.e.:
|
||||
|
||||
price='52499' # $24.99 USD
|
||||
"""
|
||||
codeName = 'ISBN'
|
||||
_attrMap = AttrMap(BASE=Ean13BarcodeWidget,
|
||||
price=AttrMapValue(
|
||||
NoneOr(nDigits(5)),
|
||||
desc='None or the price to display'),
|
||||
)
|
||||
def draw(self):
|
||||
g = Ean13BarcodeWidget.draw(self)
|
||||
|
||||
price = getattr(self,'price',None)
|
||||
if not price:
|
||||
return g
|
||||
|
||||
bounds = g.getBounds()
|
||||
x = bounds[2]
|
||||
pricecode = Ean5BarcodeWidget(x=x, value=price, price=True,
|
||||
humanReadable=True,
|
||||
barHeight=self.barHeight, quiet=self.quiet)
|
||||
g.add(pricecode)
|
||||
return g
|
||||
|
||||
def _add_human_readable(self, s, gAdd):
|
||||
Ean13BarcodeWidget._add_human_readable(self,s, gAdd)
|
||||
barWidth = self.barWidth
|
||||
barHeight = self.barHeight
|
||||
fontSize = self.fontSize
|
||||
textColor = self.textColor
|
||||
fontName = self.fontName
|
||||
fth = fontSize * 1.2
|
||||
y = self.y + 0.2 * fth + barHeight
|
||||
x = self._lquiet * barWidth
|
||||
|
||||
isbn = 'ISBN '
|
||||
segments = [s[0:3], s[3:4], s[4:9], s[9:12], s[12]]
|
||||
isbn += '-'.join(segments)
|
||||
|
||||
gAdd(String(x, y, isbn, fontName=fontName, fontSize=fontSize,
|
||||
fillColor=textColor))
|
||||
@@ -0,0 +1,445 @@
|
||||
#this code contributed by Kyle Macfarlane see
|
||||
#https://bitbucket.org/rptlab/reportlab/issues/69/implementations-of-code-128-auto-and-data
|
||||
__all__= ('ECC200datamatrix',)
|
||||
FACTORS = {
|
||||
5: (228, 48, 15, 111, 62),
|
||||
7: (23, 68, 144, 134, 240, 92, 254),
|
||||
10: (28, 24, 185, 166, 223, 248, 116, 255, 110, 61),
|
||||
11: (175, 138, 205, 12, 194, 168, 39, 245, 60, 97, 120),
|
||||
12: (41, 153, 158, 91, 61, 42, 142, 213, 97, 178, 100, 242),
|
||||
14: (156, 97, 192, 252, 95, 9, 157, 119, 138, 45, 18, 186, 83, 185),
|
||||
18: (83, 195, 100, 39, 188, 75, 66, 61, 241, 213, 109, 129,
|
||||
94, 254, 225, 48, 90, 188),
|
||||
20: (15, 195, 244, 9, 233, 71, 168, 2, 188, 160, 153, 145,
|
||||
253, 79, 108, 82, 27, 174, 186, 172),
|
||||
24: (52, 190, 88, 205, 109, 39, 176, 21, 155, 197, 251, 223, 155,
|
||||
21, 5, 172, 254, 124, 12, 181, 184, 96, 50, 193),
|
||||
28: (211, 231, 43, 97, 71, 96, 103, 174, 37, 151, 170, 53, 75, 34,
|
||||
249, 121, 17, 138, 110, 213, 141, 136, 120, 151, 233, 168, 93, 255),
|
||||
36: (245, 127, 242, 218, 130, 250, 162, 181, 102, 120, 84, 179, 220, 251,
|
||||
80, 182, 229, 18, 2, 4, 68, 33, 101, 137, 95, 119, 115, 44,
|
||||
175, 184, 59, 25, 225, 98, 81, 112),
|
||||
42: (77, 193, 137, 31, 19, 38, 22, 153, 247, 105, 122, 2, 245, 133,
|
||||
242, 8, 175, 95, 100, 9, 167, 105, 214, 111, 57, 121, 21,
|
||||
1, 253, 57, 54, 101, 248, 202, 69, 50, 150, 177, 226, 5, 9, 5),
|
||||
48: (245, 132, 172, 223, 96, 32, 117, 22, 238, 133, 238, 231, 205, 188,
|
||||
237, 87, 191, 106, 16, 147, 118, 23, 37, 90, 170, 205, 131, 88,
|
||||
120, 100, 66, 138, 186, 240, 82, 44, 176, 87, 187, 147, 160, 175,
|
||||
69, 213, 92, 253, 225, 19),
|
||||
56: (175, 9, 223, 238, 12, 17, 220, 208, 100, 29, 175, 170, 230, 192,
|
||||
215, 235, 150, 159, 36, 223, 38, 200, 132, 54, 228, 146, 218, 234,
|
||||
117, 203, 29, 232, 144, 238, 22, 150, 201, 117, 62, 207, 164, 13,
|
||||
137, 245, 127, 67, 247, 28, 155, 43, 203, 107, 233, 53, 143, 46),
|
||||
62: (242, 93, 169, 50, 144, 210, 39, 118, 202, 188, 201, 189, 143, 108,
|
||||
196, 37, 185, 112, 134, 230, 245, 63, 197, 190, 250, 106, 185, 221,
|
||||
175, 64, 114, 71, 161, 44, 147, 6, 27, 218, 51, 63, 87, 10,
|
||||
40, 130, 188, 17, 163, 31, 176, 170, 4, 107, 232, 7, 94, 166,
|
||||
224, 124, 86, 47, 11, 204),
|
||||
68: (220, 228, 173, 89, 251, 149, 159, 56, 89, 33, 147, 244, 154, 36,
|
||||
73, 127, 213, 136, 248, 180, 234, 197, 158, 177, 68, 122, 93, 213,
|
||||
15, 160, 227, 236, 66, 139, 153, 185, 202, 167, 179, 25, 220, 232,
|
||||
96, 210, 231, 136, 223, 239, 181, 241, 59, 52, 172, 25, 49, 232,
|
||||
211, 189, 64, 54, 108, 153, 132, 63, 96, 103, 82, 186)
|
||||
}
|
||||
|
||||
LOGVAL = (
|
||||
-255, 255, 1, 240, 2, 225, 241, 53, 3, 38, 226, 133, 242, 43,
|
||||
54, 210, 4, 195, 39, 114, 227, 106, 134, 28, 243, 140, 44, 23,
|
||||
55, 118, 211, 234, 5, 219, 196, 96, 40, 222, 115, 103, 228, 78,
|
||||
107, 125, 135, 8, 29, 162, 244, 186, 141, 180, 45, 99, 24, 49,
|
||||
56, 13, 119, 153, 212, 199, 235, 91, 6, 76, 220, 217, 197, 11,
|
||||
97, 184, 41, 36, 223, 253, 116, 138, 104, 193, 229, 86, 79, 171,
|
||||
108, 165, 126, 145, 136, 34, 9, 74, 30, 32, 163, 84, 245, 173,
|
||||
187, 204, 142, 81, 181, 190, 46, 88, 100, 159, 25, 231, 50, 207,
|
||||
57, 147, 14, 67, 120, 128, 154, 248, 213, 167, 200, 63, 236, 110,
|
||||
92, 176, 7, 161, 77, 124, 221, 102, 218, 95, 198, 90, 12, 152,
|
||||
98, 48, 185, 179, 42, 209, 37, 132, 224, 52, 254, 239, 117, 233,
|
||||
139, 22, 105, 27, 194, 113, 230, 206, 87, 158, 80, 189, 172, 203,
|
||||
109, 175, 166, 62, 127, 247, 146, 66, 137, 192, 35, 252, 10, 183,
|
||||
75, 216, 31, 83, 33, 73, 164, 144, 85, 170, 246, 65, 174, 61,
|
||||
188, 202, 205, 157, 143, 169, 82, 72, 182, 215, 191, 251, 47, 178,
|
||||
89, 151, 101, 94, 160, 123, 26, 112, 232, 21, 51, 238, 208, 131,
|
||||
58, 69, 148, 18, 15, 16, 68, 17, 121, 149, 129, 19, 155, 59,
|
||||
249, 70, 214, 250, 168, 71, 201, 156, 64, 60, 237, 130, 111, 20,
|
||||
93, 122, 177, 150
|
||||
)
|
||||
|
||||
ALOGVAL = (
|
||||
1, 2, 4, 8, 16, 32, 64, 128, 45, 90, 180, 69, 138, 57,
|
||||
114, 228, 229, 231, 227, 235, 251, 219, 155, 27, 54, 108, 216, 157,
|
||||
23, 46, 92, 184, 93, 186, 89, 178, 73, 146, 9, 18, 36, 72,
|
||||
144, 13, 26, 52, 104, 208, 141, 55, 110, 220, 149, 7, 14, 28,
|
||||
56, 112, 224, 237, 247, 195, 171, 123, 246, 193, 175, 115, 230, 225,
|
||||
239, 243, 203, 187, 91, 182, 65, 130, 41, 82, 164, 101, 202, 185,
|
||||
95, 190, 81, 162, 105, 210, 137, 63, 126, 252, 213, 135, 35, 70,
|
||||
140, 53, 106, 212, 133, 39, 78, 156, 21, 42, 84, 168, 125, 250,
|
||||
217, 159, 19, 38, 76, 152, 29, 58, 116, 232, 253, 215, 131, 43,
|
||||
86, 172, 117, 234, 249, 223, 147, 11, 22, 44, 88, 176, 77, 154,
|
||||
25, 50, 100, 200, 189, 87, 174, 113, 226, 233, 255, 211, 139, 59,
|
||||
118, 236, 245, 199, 163, 107, 214, 129, 47, 94, 188, 85, 170, 121,
|
||||
242, 201, 191, 83, 166, 97, 194, 169, 127, 254, 209, 143, 51, 102,
|
||||
204, 181, 71, 142, 49, 98, 196, 165, 103, 206, 177, 79, 158, 17,
|
||||
34, 68, 136, 61, 122, 244, 197, 167, 99, 198, 161, 111, 222, 145,
|
||||
15, 30, 60, 120, 240, 205, 183, 67, 134, 33, 66, 132, 37, 74,
|
||||
148, 5, 10, 20, 40, 80, 160, 109, 218, 153, 31, 62, 124, 248,
|
||||
221, 151, 3, 6, 12, 24, 48, 96, 192, 173, 119, 238, 241, 207,
|
||||
179, 75, 150, 1
|
||||
)
|
||||
|
||||
from reportlab.graphics.barcode.common import Barcode
|
||||
class ECC200DataMatrix(Barcode):
|
||||
'''This code only supports a Type 12 (44x44) C40 encoded data matrix.
|
||||
This is the size and encoding that Royal Mail wants on all mail from October 1st 2015.
|
||||
see https://bitbucket.org/rptlab/reportlab/issues/69/implementations-of-code-128-auto-and-data
|
||||
'''
|
||||
barWidth = 4
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Barcode.__init__(self,*args, **kwargs)
|
||||
|
||||
# These values below are hardcoded for a Type 12 44x44 data matrix
|
||||
self.row_modules = 44
|
||||
self.col_modules = 44
|
||||
self.row_regions = 2
|
||||
self.col_regions = 2
|
||||
self.cw_data = 144
|
||||
self.cw_ecc = 56
|
||||
self.row_usable_modules = self.row_modules - self.row_regions * 2
|
||||
self.col_usable_modules = self.col_modules - self.col_regions * 2
|
||||
|
||||
def validate(self):
|
||||
self.valid = 1
|
||||
for c in self.value:
|
||||
if ord(c) > 255:
|
||||
self.valid = 0
|
||||
break
|
||||
else:
|
||||
self.validated = self.value
|
||||
|
||||
def _encode_c40_char(self, char):
|
||||
o = ord(char)
|
||||
encoded = []
|
||||
|
||||
if o == 32 or (o >= 48 and o <= 57) or (o >= 65 and o <= 90):
|
||||
# Stay in set 0
|
||||
if o == 32:
|
||||
encoded.append(o - 29)
|
||||
elif o >= 48 and o <= 57:
|
||||
encoded.append(o - 44)
|
||||
else:
|
||||
encoded.append(o - 51)
|
||||
elif o >= 0 and o <= 31:
|
||||
encoded.append(0) # Shift to set 1
|
||||
encoded.append(o)
|
||||
elif (o >= 33 and o <= 64) or (o >= 91 and o <= 95):
|
||||
encoded.append(1) # Shift to set 2
|
||||
if o >= 33 and o <= 64:
|
||||
encoded.append(o - 33)
|
||||
else:
|
||||
encoded.append(o - 69)
|
||||
elif o >= 96 and o <= 127:
|
||||
encoded.append(2) # Shift to set 3
|
||||
encoded.append(o - 96)
|
||||
elif o >= 128 and o <= 255:
|
||||
# Extended ASCII
|
||||
encoded.append(1) # Shift to set 2
|
||||
encoded.append(30) # Upper shift / hibit
|
||||
encoded += self._encode_c40_char(chr(o - 128))
|
||||
else:
|
||||
raise Exception('Cannot encode %s (%s)' % (char, o))
|
||||
|
||||
return encoded
|
||||
|
||||
def _encode_c40(self, value):
|
||||
encoded = []
|
||||
|
||||
for c in value:
|
||||
encoded += self._encode_c40_char(c)
|
||||
|
||||
while len(encoded) % 3:
|
||||
encoded.append(0) # Fake padding that makes chunking in the next step easier
|
||||
|
||||
codewords = []
|
||||
codewords.append(230) # Switch to C40 encoding
|
||||
|
||||
for i in range(0, len(encoded), 3):
|
||||
chunk = encoded[i:i+3]
|
||||
total = chunk[0] * 1600 + chunk[1] * 40 + chunk[2] + 1
|
||||
codewords.append(total // 256)
|
||||
codewords.append(total % 256)
|
||||
|
||||
codewords.append(254) # End of data
|
||||
|
||||
if len(codewords) > self.cw_data:
|
||||
raise Exception('Too much data to fit into a data matrix of this size')
|
||||
|
||||
if len(codewords) < self.cw_data:
|
||||
# Real padding
|
||||
codewords.append(129) # Start padding
|
||||
while len(codewords) < self.cw_data:
|
||||
r = ((149 * (len(codewords) + 1)) % 253) + 1
|
||||
codewords.append((129 + r) % 254)
|
||||
|
||||
return codewords
|
||||
|
||||
def _gfsum(self, int1, int2):
|
||||
return int1 ^ int2
|
||||
|
||||
def _gfproduct(self, int1, int2):
|
||||
if int1 == 0 or int2 == 0:
|
||||
return 0
|
||||
else:
|
||||
return ALOGVAL[(LOGVAL[int1] + LOGVAL[int2]) % 255]
|
||||
|
||||
def _get_reed_solomon_code(self, data, num_code_words):
|
||||
"""
|
||||
This method is basically verbatim from "huBarcode" which is BSD licensed
|
||||
https://github.com/hudora/huBarcode/blob/master/hubarcode/datamatrix/reedsolomon.py
|
||||
"""
|
||||
cw_factors = FACTORS[num_code_words]
|
||||
code_words = [0] * num_code_words
|
||||
|
||||
for data_word in data:
|
||||
tmp = self._gfsum(data_word, code_words[-1])
|
||||
for j in range(num_code_words - 1, -1, -1):
|
||||
code_words[j] = self._gfproduct(tmp, cw_factors[j])
|
||||
if j > 0:
|
||||
code_words[j] = self._gfsum(code_words[j - 1], code_words[j])
|
||||
|
||||
code_words.reverse()
|
||||
return code_words
|
||||
|
||||
def _get_next_bits(self, data):
|
||||
value = data.pop(0)
|
||||
bits = []
|
||||
for i in range(0, 8):
|
||||
bits.append(value >> i & 1)
|
||||
bits.reverse()
|
||||
return bits
|
||||
|
||||
def _place_bit(self, row, col, bit):
|
||||
if row < 0:
|
||||
row += self.row_usable_modules
|
||||
col += (4 - ((self.row_usable_modules + 4) % 8))
|
||||
|
||||
if col < 0:
|
||||
col += self.col_usable_modules
|
||||
row += (4 - ((self.col_usable_modules + 4) % 8))
|
||||
|
||||
self._matrix[row][col] = bit
|
||||
|
||||
def _place_bit_corner_1(self, data):
|
||||
bits = self._get_next_bits(data)
|
||||
self._place_bit(self.row_usable_modules - 1, 0, bits[0])
|
||||
self._place_bit(self.row_usable_modules - 1, 1, bits[1])
|
||||
self._place_bit(self.row_usable_modules - 1, 2, bits[2])
|
||||
self._place_bit(0, self.col_usable_modules - 2, bits[3])
|
||||
self._place_bit(0, self.col_usable_modules - 1, bits[4])
|
||||
self._place_bit(1, self.col_usable_modules - 1, bits[5])
|
||||
self._place_bit(2, self.col_usable_modules - 1, bits[6])
|
||||
self._place_bit(3, self.col_usable_modules - 1, bits[7])
|
||||
|
||||
def _place_bit_corner_2(self, data):
|
||||
bits = self._get_next_bits(data)
|
||||
self._place_bit(self.row_usable_modules - 3, 0, bits[0])
|
||||
self._place_bit(self.row_usable_modules - 2, 0, bits[1])
|
||||
self._place_bit(self.row_usable_modules - 1, 0, bits[2])
|
||||
self._place_bit(0, self.col_usable_modules - 4, bits[3])
|
||||
self._place_bit(0, self.col_usable_modules - 3, bits[4])
|
||||
self._place_bit(0, self.col_usable_modules - 2, bits[5])
|
||||
self._place_bit(0, self.col_usable_modules - 1, bits[6])
|
||||
self._place_bit(1, self.col_usable_modules - 1, bits[7])
|
||||
|
||||
def _place_bit_corner_3(self, data):
|
||||
bits = self._get_next_bits(data)
|
||||
self._place_bit(self.row_usable_modules - 3, 0, bits[0])
|
||||
self._place_bit(self.row_usable_modules - 2, 0, bits[1])
|
||||
self._place_bit(self.row_usable_modules - 1, 0, bits[2])
|
||||
self._place_bit(0, self.col_usable_modules - 2, bits[3])
|
||||
self._place_bit(0, self.col_usable_modules - 1, bits[4])
|
||||
self._place_bit(1, self.col_usable_modules - 1, bits[5])
|
||||
self._place_bit(2, self.col_usable_modules - 1, bits[6])
|
||||
self._place_bit(3, self.col_usable_modules - 1, bits[7])
|
||||
|
||||
def _place_bit_corner_4(self, data):
|
||||
bits = self._get_next_bits(data)
|
||||
self._place_bit(self.row_usable_modules - 1, 0, bits[0])
|
||||
self._place_bit(self.row_usable_modules - 1, self.col_usable_modules - 1, bits[1])
|
||||
self._place_bit(0, self.col_usable_modules - 3, bits[2])
|
||||
self._place_bit(0, self.col_usable_modules - 2, bits[3])
|
||||
self._place_bit(0, self.col_usable_modules - 1, bits[4])
|
||||
self._place_bit(1, self.col_usable_modules - 3, bits[5])
|
||||
self._place_bit(1, self.col_usable_modules - 2, bits[6])
|
||||
self._place_bit(1, self.col_usable_modules - 1, bits[7])
|
||||
|
||||
def _place_bit_standard(self, data, row, col):
|
||||
bits = self._get_next_bits(data)
|
||||
self._place_bit(row - 2, col - 2, bits[0])
|
||||
self._place_bit(row - 2, col - 1, bits[1])
|
||||
self._place_bit(row - 1, col - 2, bits[2])
|
||||
self._place_bit(row - 1, col - 1, bits[3])
|
||||
self._place_bit(row - 1, col, bits[4])
|
||||
self._place_bit(row, col - 2, bits[5])
|
||||
self._place_bit(row, col - 1, bits[6])
|
||||
self._place_bit(row, col, bits[7])
|
||||
|
||||
def _create_matrix(self, data):
|
||||
"""
|
||||
This method is heavily influenced by "huBarcode" which is BSD licensed
|
||||
https://github.com/hudora/huBarcode/blob/master/hubarcode/datamatrix/placement.py
|
||||
"""
|
||||
rows = self.row_usable_modules
|
||||
cols = self.col_usable_modules
|
||||
|
||||
self._matrix = self._create_empty_matrix(rows, cols)
|
||||
|
||||
row = 4
|
||||
col = 0
|
||||
|
||||
while True:
|
||||
if row == rows and col == 0:
|
||||
self._place_bit_corner_1(data)
|
||||
elif row == (rows - 2) and col == 0 and (cols % 4):
|
||||
self._place_bit_corner_2(data)
|
||||
elif row == (rows - 2) and col == 0 and (cols % 8 == 4):
|
||||
self._place_bit_corner_3(data)
|
||||
elif row == (rows + 4) and col == 2 and (cols % 8 == 0):
|
||||
self._place_bit_corner_4(data)
|
||||
|
||||
while True:
|
||||
if row < rows and col >= 0 and self._matrix[row][col] is None:
|
||||
self._place_bit_standard(data, row, col)
|
||||
|
||||
row -= 2
|
||||
col += 2
|
||||
|
||||
if row < 0 or col >= cols:
|
||||
break
|
||||
|
||||
row += 1
|
||||
col += 3
|
||||
|
||||
while True:
|
||||
if row >= 0 and col < cols and self._matrix[row][col] is None:
|
||||
self._place_bit_standard(data, row, col)
|
||||
|
||||
row += 2
|
||||
col -= 2
|
||||
|
||||
if row >= rows or col < 0:
|
||||
break
|
||||
|
||||
row += 3
|
||||
col += 1
|
||||
|
||||
if row >= rows and col >= cols:
|
||||
break
|
||||
|
||||
for row in self._matrix:
|
||||
for i in range(0, cols):
|
||||
if row[i] is None:
|
||||
row[i] = 0
|
||||
|
||||
return self._matrix
|
||||
|
||||
def _create_data_regions(self, matrix):
|
||||
regions = []
|
||||
col_offset = 0
|
||||
row_offset = 0
|
||||
|
||||
rows = int(self.row_usable_modules / self.row_regions)
|
||||
cols = int(self.col_usable_modules / self.col_regions)
|
||||
|
||||
while col_offset < self.row_regions:
|
||||
while row_offset < self.col_regions:
|
||||
r_offset = col_offset * rows
|
||||
c_offset = row_offset * cols
|
||||
region = matrix[r_offset:rows+r_offset]
|
||||
for i in range(0, len(region)):
|
||||
region[i] = region[i][c_offset:cols+c_offset]
|
||||
regions.append(region)
|
||||
row_offset += 1
|
||||
row_offset = 0
|
||||
col_offset += 1
|
||||
|
||||
return regions
|
||||
|
||||
def _create_empty_matrix(self, row, col):
|
||||
matrix = []
|
||||
for i in range(0, row):
|
||||
matrix.append([None] * col)
|
||||
return matrix
|
||||
|
||||
def _wrap_data_regions_with_finders(self, regions):
|
||||
wrapped = []
|
||||
|
||||
for region in regions:
|
||||
matrix = self._create_empty_matrix(
|
||||
int(self.col_modules / self.col_regions),
|
||||
int(self.row_modules / self.row_regions)
|
||||
)
|
||||
|
||||
for i, rows in enumerate(region):
|
||||
for j, data in enumerate(rows):
|
||||
matrix[i+1][j+1] = data
|
||||
|
||||
for i, row in enumerate(matrix):
|
||||
if i == 0:
|
||||
for j, col in enumerate(row):
|
||||
row[j] = (j + 1) % 2
|
||||
elif i + 1 == len(matrix):
|
||||
for j, col in enumerate(row):
|
||||
row[j] = 1
|
||||
else:
|
||||
row[0] = 1
|
||||
row[-1] = i % 2
|
||||
|
||||
wrapped.append(matrix)
|
||||
|
||||
return wrapped
|
||||
|
||||
def _merge_data_regions(self, regions):
|
||||
merged = []
|
||||
|
||||
for i in range(0, len(regions), self.row_regions):
|
||||
chunk = regions[i:i+self.row_regions]
|
||||
j = 0
|
||||
while j < len(chunk[0]):
|
||||
merged_row = []
|
||||
for row in chunk:
|
||||
merged_row += row[j]
|
||||
merged.append(merged_row)
|
||||
j += 1
|
||||
|
||||
return merged
|
||||
|
||||
def encode(self):
|
||||
if hasattr(self, 'encoded'):
|
||||
return self.encoded
|
||||
|
||||
encoded = self._encode_c40(self.validated)
|
||||
encoded += self._get_reed_solomon_code(encoded, self.cw_ecc)
|
||||
|
||||
matrix = self._create_matrix(encoded)
|
||||
data_regions = self._create_data_regions(matrix)
|
||||
wrapped = self._wrap_data_regions_with_finders(data_regions)
|
||||
self.encoded = self._merge_data_regions(wrapped)
|
||||
|
||||
self.encoded.reverse() # Helpful since PDFs start at bottom left corner
|
||||
|
||||
return self.encoded
|
||||
|
||||
def computeSize(self, *args):
|
||||
self._height = self.row_modules * self.barWidth
|
||||
self._width = self.col_modules * self.barWidth
|
||||
|
||||
def draw(self):
|
||||
for y, row in enumerate(self.encoded):
|
||||
for x, data in enumerate(row):
|
||||
if data:
|
||||
self.rect(
|
||||
self.x + x * self.barWidth,
|
||||
self.y + y * self.barWidth,
|
||||
self.barWidth,
|
||||
self.barWidth
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
#
|
||||
# Copyright (c) 2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. All advertising materials mentioning features or use of this software
|
||||
# must display the following acknowledgement:
|
||||
# This product includes software developed by Tyler C. Sarna.
|
||||
# 4. Neither the name of the author nor the names of contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
# . 3 T Tracker
|
||||
# , 2 D Descender
|
||||
# ' 1 A Ascender
|
||||
# | 0 H Ascender/Descender
|
||||
|
||||
_rm_patterns = {
|
||||
"0" : "--||", "1" : "-',|", "2" : "-'|,", "3" : "'-,|",
|
||||
"4" : "'-|,", "5" : "'',,", "6" : "-,'|", "7" : "-|-|",
|
||||
"8" : "-|',", "9" : "',-|", "A" : "',',", "B" : "'|-,",
|
||||
"C" : "-,|'", "D" : "-|,'", "E" : "-||-", "F" : "',,'",
|
||||
"G" : "',|-", "H" : "'|,-", "I" : ",-'|", "J" : ",'-|",
|
||||
"K" : ",'',", "L" : "|--|", "M" : "|-',", "N" : "|'-,",
|
||||
"O" : ",-|'", "P" : ",','", "Q" : ",'|-", "R" : "|-,'",
|
||||
"S" : "|-|-", "T" : "|',-", "U" : ",,''", "V" : ",|-'",
|
||||
"W" : ",|'-", "X" : "|,-'", "Y" : "|,'-", "Z" : "||--",
|
||||
|
||||
# start, stop
|
||||
"(" : "'-,'", ")" : "'|,|"
|
||||
}
|
||||
|
||||
_ozN_patterns = {
|
||||
"0" : "||", "1" : "|'", "2" : "|,", "3" : "'|", "4" : "''",
|
||||
"5" : "',", "6" : ",|", "7" : ",'", "8" : ",,", "9" : ".|"
|
||||
}
|
||||
|
||||
_ozC_patterns = {
|
||||
"A" : "|||", "B" : "||'", "C" : "||,", "D" : "|'|",
|
||||
"E" : "|''", "F" : "|',", "G" : "|,|", "H" : "|,'",
|
||||
"I" : "|,,", "J" : "'||", "K" : "'|'", "L" : "'|,",
|
||||
"M" : "''|", "N" : "'''", "O" : "'',", "P" : "',|",
|
||||
"Q" : "','", "R" : "',,", "S" : ",||", "T" : ",|'",
|
||||
"U" : ",|,", "V" : ",'|", "W" : ",''", "X" : ",',",
|
||||
"Y" : ",,|", "Z" : ",,'", "a" : "|,.", "b" : "|.|",
|
||||
"c" : "|.'", "d" : "|.,", "e" : "|..", "f" : "'|.",
|
||||
"g" : "''.", "h" : "',.", "i" : "'.|", "j" : "'.'",
|
||||
"k" : "'.,", "l" : "'..", "m" : ",|.", "n" : ",'.",
|
||||
"o" : ",,.", "p" : ",.|", "q" : ",.'", "r" : ",.,",
|
||||
"s" : ",..", "t" : ".|.", "u" : ".'.", "v" : ".,.",
|
||||
"w" : "..|", "x" : "..'", "y" : "..,", "z" : "...",
|
||||
"0" : ",,,", "1" : ".||", "2" : ".|'", "3" : ".|,",
|
||||
"4" : ".'|", "5" : ".''", "6" : ".',", "7" : ".,|",
|
||||
"8" : ".,'", "9" : ".,,", " " : "||.", "#" : "|'.",
|
||||
}
|
||||
|
||||
#http://www.auspost.com.au/futurepost/
|
||||
@@ -0,0 +1,195 @@
|
||||
# (c) 2008 Jerome Alet - <alet@librelogiciel.com>
|
||||
# Licensing terms : ReportLab's license.
|
||||
|
||||
from reportlab.graphics.barcode.code39 import Standard39
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.units import cm
|
||||
from string import ascii_uppercase, digits as string_digits
|
||||
|
||||
class BaseLTOLabel(Standard39) :
|
||||
"""
|
||||
Base class for LTO labels.
|
||||
|
||||
Specification taken from "IBM LTO Ultrium Cartridge Label Specification, Revision 3"
|
||||
available on May 14th 2008 from :
|
||||
http://www-1.ibm.com/support/docview.wss?rs=543&context=STCVQ6R&q1=ssg1*&uid=ssg1S7000429&loc=en_US&cs=utf-8&lang=en+en
|
||||
"""
|
||||
LABELWIDTH = 7.9 * cm
|
||||
LABELHEIGHT = 1.7 * cm
|
||||
LABELROUND = 0.15 * cm
|
||||
CODERATIO = 2.75
|
||||
CODENOMINALWIDTH = 7.4088 * cm
|
||||
CODEBARHEIGHT = 1.11 * cm
|
||||
CODEBARWIDTH = 0.0432 * cm
|
||||
CODEGAP = CODEBARWIDTH
|
||||
CODELQUIET = 10 * CODEBARWIDTH
|
||||
CODERQUIET = 10 * CODEBARWIDTH
|
||||
def __init__(self, prefix="",
|
||||
number=None,
|
||||
subtype="1",
|
||||
border=None,
|
||||
checksum=False,
|
||||
availheight=None) :
|
||||
"""
|
||||
Initializes an LTO label.
|
||||
|
||||
prefix : Up to six characters from [A-Z][0-9]. Defaults to "".
|
||||
number : Label's number or None. Defaults to None.
|
||||
subtype : LTO subtype string , e.g. "1" for LTO1. Defaults to "1".
|
||||
border : None, or the width of the label's border. Defaults to None.
|
||||
checksum : Boolean indicates if checksum char has to be printed. Defaults to False.
|
||||
availheight : Available height on the label, or None for automatic. Defaults to None.
|
||||
"""
|
||||
self.height = max(availheight, self.CODEBARHEIGHT)
|
||||
self.border = border
|
||||
if (len(subtype) != 1) \
|
||||
or (subtype not in ascii_uppercase + string_digits) :
|
||||
raise ValueError("Invalid subtype '%s'" % subtype)
|
||||
if ((not number) and (len(prefix) > 6)) \
|
||||
or not prefix.isalnum() :
|
||||
raise ValueError("Invalid prefix '%s'" % prefix)
|
||||
label = "%sL%s" % ((prefix + str(number or 0).zfill(6 - len(prefix)))[:6],
|
||||
subtype)
|
||||
if len(label) != 8 :
|
||||
raise ValueError("Invalid set of parameters (%s, %s, %s)" \
|
||||
% (prefix, number, subtype))
|
||||
self.label = label
|
||||
Standard39.__init__(self,
|
||||
label,
|
||||
ratio=self.CODERATIO,
|
||||
barHeight=self.height,
|
||||
barWidth=self.CODEBARWIDTH,
|
||||
gap=self.CODEGAP,
|
||||
lquiet=self.CODELQUIET,
|
||||
rquiet=self.CODERQUIET,
|
||||
quiet=True,
|
||||
checksum=checksum)
|
||||
|
||||
def drawOn(self, canvas, x, y) :
|
||||
"""Draws the LTO label onto the canvas."""
|
||||
canvas.saveState()
|
||||
canvas.translate(x, y)
|
||||
if self.border :
|
||||
canvas.setLineWidth(self.border)
|
||||
canvas.roundRect(0, 0,
|
||||
self.LABELWIDTH,
|
||||
self.LABELHEIGHT,
|
||||
self.LABELROUND)
|
||||
Standard39.drawOn(self,
|
||||
canvas,
|
||||
(self.LABELWIDTH-self.CODENOMINALWIDTH)/2.0,
|
||||
self.LABELHEIGHT-self.height)
|
||||
canvas.restoreState()
|
||||
|
||||
class VerticalLTOLabel(BaseLTOLabel) :
|
||||
"""
|
||||
A class for LTO labels with rectangular blocks around the tape identifier.
|
||||
"""
|
||||
LABELFONT = ("Helvetica-Bold", 14)
|
||||
BLOCKWIDTH = 1*cm
|
||||
BLOCKHEIGHT = 0.45*cm
|
||||
LINEWIDTH = 0.0125
|
||||
NBBLOCKS = 7
|
||||
COLORSCHEME = ("red",
|
||||
"yellow",
|
||||
"lightgreen",
|
||||
"lightblue",
|
||||
"grey",
|
||||
"orangered",
|
||||
"pink",
|
||||
"darkgreen",
|
||||
"orange",
|
||||
"purple")
|
||||
|
||||
def __init__(self, *args, **kwargs) :
|
||||
"""
|
||||
Initializes the label.
|
||||
|
||||
colored : boolean to determine if blocks have to be colorized.
|
||||
"""
|
||||
if "colored" in kwargs:
|
||||
self.colored = kwargs["colored"]
|
||||
del kwargs["colored"]
|
||||
else :
|
||||
self.colored = False
|
||||
kwargs["availheight"] = self.LABELHEIGHT-self.BLOCKHEIGHT
|
||||
BaseLTOLabel.__init__(self, *args, **kwargs)
|
||||
|
||||
def drawOn(self, canvas, x, y) :
|
||||
"""Draws some blocks around the identifier's characters."""
|
||||
BaseLTOLabel.drawOn(self,
|
||||
canvas,
|
||||
x,
|
||||
y)
|
||||
canvas.saveState()
|
||||
canvas.setLineWidth(self.LINEWIDTH)
|
||||
canvas.setStrokeColorRGB(0, 0, 0)
|
||||
canvas.translate(x, y)
|
||||
xblocks = (self.LABELWIDTH-(self.NBBLOCKS*self.BLOCKWIDTH))/2.0
|
||||
for i in range(self.NBBLOCKS) :
|
||||
(font, size) = self.LABELFONT
|
||||
newfont = self.LABELFONT
|
||||
if i == (self.NBBLOCKS - 1) :
|
||||
part = self.label[i:]
|
||||
(font, size) = newfont
|
||||
size /= 2.0
|
||||
newfont = (font, size)
|
||||
else :
|
||||
part = self.label[i]
|
||||
canvas.saveState()
|
||||
canvas.translate(xblocks+(i*self.BLOCKWIDTH), 0)
|
||||
if self.colored and part.isdigit() :
|
||||
canvas.setFillColorRGB(*getattr(colors,
|
||||
self.COLORSCHEME[int(part)],
|
||||
colors.Color(1, 1, 1)).rgb())
|
||||
else:
|
||||
canvas.setFillColorRGB(1, 1, 1)
|
||||
canvas.rect(0, 0, self.BLOCKWIDTH, self.BLOCKHEIGHT, fill=True)
|
||||
canvas.translate((self.BLOCKWIDTH+canvas.stringWidth(part, *newfont))/2.0,
|
||||
(self.BLOCKHEIGHT/2.0))
|
||||
canvas.rotate(90.0)
|
||||
canvas.setFont(*newfont)
|
||||
canvas.setFillColorRGB(0, 0, 0)
|
||||
canvas.drawCentredString(0, 0, part)
|
||||
canvas.restoreState()
|
||||
canvas.restoreState()
|
||||
|
||||
def test() :
|
||||
"""Test this."""
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.lib import pagesizes
|
||||
|
||||
canvas = Canvas("labels.pdf", pagesize=pagesizes.A4)
|
||||
canvas.setFont("Helvetica", 30)
|
||||
(width, height) = pagesizes.A4
|
||||
canvas.drawCentredString(width/2.0, height-4*cm, "Sample LTO labels")
|
||||
xpos = xorig = 2 * cm
|
||||
ypos = yorig = 2 * cm
|
||||
colwidth = 10 * cm
|
||||
lineheight = 3.9 * cm
|
||||
count = 1234
|
||||
BaseLTOLabel("RL", count, "3").drawOn(canvas, xpos, ypos)
|
||||
ypos += lineheight
|
||||
count += 1
|
||||
BaseLTOLabel("RL", count, "3",
|
||||
border=0.0125).drawOn(canvas, xpos, ypos)
|
||||
ypos += lineheight
|
||||
count += 1
|
||||
VerticalLTOLabel("RL", count, "3").drawOn(canvas, xpos, ypos)
|
||||
ypos += lineheight
|
||||
count += 1
|
||||
VerticalLTOLabel("RL", count, "3",
|
||||
border=0.0125).drawOn(canvas, xpos, ypos)
|
||||
ypos += lineheight
|
||||
count += 1
|
||||
VerticalLTOLabel("RL", count, "3",
|
||||
colored=True).drawOn(canvas, xpos, ypos)
|
||||
ypos += lineheight
|
||||
count += 1
|
||||
VerticalLTOLabel("RL", count, "3",
|
||||
border=0.0125, colored=True).drawOn(canvas, xpos, ypos)
|
||||
canvas.showPage()
|
||||
canvas.save()
|
||||
|
||||
if __name__ == "__main__" :
|
||||
test()
|
||||
@@ -0,0 +1,197 @@
|
||||
#
|
||||
# ReportLab QRCode widget
|
||||
#
|
||||
# Ported from the Javascript library QRCode for Javascript by Sam Curren
|
||||
#
|
||||
# URL: http://www.d-project.com/
|
||||
# http://d-project.googlecode.com/svn/trunk/misc/qrcode/js/qrcode.js
|
||||
# qrcode.js is copyright (c) 2009 Kazuhiko Arase
|
||||
#
|
||||
# Original ReportLab module by German M. Bravo
|
||||
#
|
||||
# modified and improved by Anders Hammarquist <iko@openend.se>
|
||||
# and used with permission under the ReportLab License
|
||||
#
|
||||
# The word "QR Code" is registered trademark of
|
||||
# DENSO WAVE INCORPORATED
|
||||
# http://www.denso-wave.com/qrcode/faqpatent-e.html
|
||||
|
||||
__all__ = ('QrCodeWidget')
|
||||
|
||||
import itertools
|
||||
|
||||
from reportlab.platypus.flowables import Flowable
|
||||
from reportlab.graphics.shapes import Group, Rect
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isNumberOrNone, isColor, Validator
|
||||
from reportlab.lib.attrmap import AttrMap, AttrMapValue
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import asUnicodeEx, isUnicode
|
||||
from reportlab.graphics.barcode import qrencoder
|
||||
|
||||
class isLevel(Validator):
|
||||
def test(self, x):
|
||||
return x in ['L', 'M', 'Q', 'H']
|
||||
isLevel = isLevel()
|
||||
|
||||
class isUnicodeOrQRList(Validator):
|
||||
def _test(self, x):
|
||||
if isUnicode(x):
|
||||
return True
|
||||
if all(isinstance(v, qrencoder.QR) for v in x):
|
||||
return True
|
||||
return False
|
||||
|
||||
def test(self, x):
|
||||
return self._test(x) or self.normalizeTest(x)
|
||||
|
||||
def normalize(self, x):
|
||||
if self._test(x):
|
||||
return x
|
||||
try:
|
||||
return asUnicodeEx(x)
|
||||
except UnicodeError:
|
||||
raise ValueError("Can't convert to unicode: %r" % x)
|
||||
isUnicodeOrQRList = isUnicodeOrQRList()
|
||||
|
||||
class SRect(Rect):
|
||||
def __init__(self, x, y, width, height, fillColor=colors.black):
|
||||
Rect.__init__(self, x, y, width, height, fillColor=fillColor,
|
||||
strokeColor=None, strokeWidth=0)
|
||||
|
||||
class QrCodeWidget(Widget):
|
||||
codeName = "QR"
|
||||
_attrMap = AttrMap(
|
||||
BASE = Widget,
|
||||
value = AttrMapValue(isUnicodeOrQRList, desc='QRCode data'),
|
||||
x = AttrMapValue(isNumber, desc='x-coord'),
|
||||
y = AttrMapValue(isNumber, desc='y-coord'),
|
||||
barFillColor = AttrMapValue(isColor, desc='bar color'),
|
||||
barWidth = AttrMapValue(isNumber, desc='Width of bars.'), # maybe should be named just width?
|
||||
barHeight = AttrMapValue(isNumber, desc='Height of bars.'), # maybe should be named just height?
|
||||
barBorder = AttrMapValue(isNumber, desc='Width of QR border.'), # maybe should be named qrBorder?
|
||||
barLevel = AttrMapValue(isLevel, desc='QR Code level.'), # maybe should be named qrLevel
|
||||
qrVersion = AttrMapValue(isNumberOrNone, desc='QR Code version. None for auto'),
|
||||
# Below are ignored, they make no sense
|
||||
barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'),
|
||||
barStrokeColor = AttrMapValue(isColor, desc='Color of bar borders.'),
|
||||
)
|
||||
x = 0
|
||||
y = 0
|
||||
barFillColor = colors.black
|
||||
barStrokeColor = None
|
||||
barStrokeWidth = 0
|
||||
barHeight = 32*mm
|
||||
barWidth = 32*mm
|
||||
barBorder = 4
|
||||
barLevel = 'L'
|
||||
qrVersion = None
|
||||
value = None
|
||||
|
||||
def __init__(self, value='Hello World', **kw):
|
||||
self.value = isUnicodeOrQRList.normalize(value)
|
||||
for k, v in kw.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
ec_level = getattr(qrencoder.QRErrorCorrectLevel, self.barLevel)
|
||||
|
||||
self.__dict__['qr'] = qrencoder.QRCode(self.qrVersion, ec_level)
|
||||
|
||||
if isUnicode(self.value):
|
||||
self.addData(self.value)
|
||||
elif self.value:
|
||||
for v in self.value:
|
||||
self.addData(v)
|
||||
|
||||
def addData(self, value):
|
||||
self.qr.addData(value)
|
||||
|
||||
def draw(self):
|
||||
self.qr.make()
|
||||
|
||||
g = Group()
|
||||
|
||||
color = self.barFillColor
|
||||
border = self.barBorder
|
||||
width = self.barWidth
|
||||
height = self.barHeight
|
||||
x = self.x
|
||||
y = self.y
|
||||
|
||||
g.add(SRect(x, y, width, height, fillColor=None))
|
||||
|
||||
moduleCount = self.qr.getModuleCount()
|
||||
minwh = float(min(width, height))
|
||||
boxsize = minwh / (moduleCount + border * 2.0)
|
||||
offsetX = x + (width - minwh) / 2.0
|
||||
offsetY = y + (minwh - height) / 2.0
|
||||
|
||||
for r, row in enumerate(self.qr.modules):
|
||||
row = map(bool, row)
|
||||
c = 0
|
||||
for t, tt in itertools.groupby(row):
|
||||
isDark = t
|
||||
count = len(list(tt))
|
||||
if isDark:
|
||||
x = (c + border) * boxsize
|
||||
y = (r + border + 1) * boxsize
|
||||
s = SRect(offsetX + x, offsetY + height - y, count * boxsize, boxsize,
|
||||
fillColor=color)
|
||||
g.add(s)
|
||||
c += count
|
||||
|
||||
return g
|
||||
|
||||
|
||||
# Flowable version
|
||||
|
||||
class QrCode(Flowable):
|
||||
height = 32*mm
|
||||
width = 32*mm
|
||||
qrBorder = 4
|
||||
qrLevel = 'L'
|
||||
qrVersion = None
|
||||
value = None
|
||||
|
||||
def __init__(self, value=None, **kw):
|
||||
self.value = isUnicodeOrQRList.normalize(value)
|
||||
|
||||
for k, v in kw.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
ec_level = getattr(qrencoder.QRErrorCorrectLevel, self.qrLevel)
|
||||
|
||||
self.qr = qrencoder.QRCode(self.qrVersion, ec_level)
|
||||
|
||||
if isUnicode(self.value):
|
||||
self.addData(self.value)
|
||||
elif self.value:
|
||||
for v in self.value:
|
||||
self.addData(v)
|
||||
|
||||
def addData(self, value):
|
||||
self.qr.addData(value)
|
||||
|
||||
def draw(self):
|
||||
self.qr.make()
|
||||
|
||||
moduleCount = self.qr.getModuleCount()
|
||||
border = self.qrBorder
|
||||
xsize = self.width / (moduleCount + border * 2.0)
|
||||
ysize = self.height / (moduleCount + border * 2.0)
|
||||
|
||||
for r, row in enumerate(self.qr.modules):
|
||||
row = map(bool, row)
|
||||
c = 0
|
||||
for t, tt in itertools.groupby(row):
|
||||
isDark = t
|
||||
count = len(list(tt))
|
||||
if isDark:
|
||||
x = (c + border) * xsize
|
||||
y = self.height - (r + border + 1) * ysize
|
||||
self.rect(x, y, count * xsize, ysize * 1.05)
|
||||
c += count
|
||||
|
||||
def rect(self, x, y, w, h):
|
||||
self.canv.rect(x, y, w, h, stroke=0, fill=1)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
||||
#!/usr/pkg/bin/python
|
||||
|
||||
import sys, time
|
||||
|
||||
from reportlab import Version as __RL_Version__
|
||||
from reportlab.graphics.barcode.common import *
|
||||
from reportlab.graphics.barcode.code39 import *
|
||||
from reportlab.graphics.barcode.code93 import *
|
||||
from reportlab.graphics.barcode.code128 import *
|
||||
from reportlab.graphics.barcode.usps import *
|
||||
from reportlab.graphics.barcode.usps4s import USPS_4State
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.barcode.dmtx import DataMatrixWidget, pylibdmtx
|
||||
|
||||
from reportlab.platypus import Spacer, SimpleDocTemplate, PageBreak
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib import colors
|
||||
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.platypus.paragraph import Paragraph
|
||||
from reportlab.platypus.flowables import XBox, KeepTogether
|
||||
from reportlab.graphics.shapes import Drawing, Rect, Line
|
||||
|
||||
from reportlab.graphics.barcode import getCodeNames, createBarcodeDrawing, createBarcodeImageInMemory
|
||||
|
||||
def run():
|
||||
styles = getSampleStyleSheet()
|
||||
styleN = styles['Normal']
|
||||
styleH = styles['Heading1']
|
||||
story = []
|
||||
storyAdd = story.append
|
||||
|
||||
#for codeNames in code
|
||||
storyAdd(Paragraph('I2of5', styleN))
|
||||
storyAdd(I2of5('05400141288766', barWidth = inch*0.02, checksum=0))
|
||||
storyAdd(I2of5('0001234567890', barWidth = inch*0.02, checksum=1))
|
||||
storyAdd(I2of5('00012345678905', barWidth = inch*0.02, checksum=0))
|
||||
storyAdd(I2of5('0001234567890', barWidth = inch*0.02, checksum=1, bearerBox = True))
|
||||
|
||||
|
||||
storyAdd(Paragraph('MSI', styleN))
|
||||
storyAdd(MSI(1234))
|
||||
|
||||
storyAdd(Paragraph('Codabar', styleN))
|
||||
storyAdd(Codabar("A012345B", barWidth = inch*0.02))
|
||||
|
||||
storyAdd(Paragraph('Code 11', styleN))
|
||||
storyAdd(Code11("01234545634563"))
|
||||
|
||||
storyAdd(Paragraph('Code 39', styleN))
|
||||
storyAdd(Standard39("A012345B%R"))
|
||||
|
||||
storyAdd(Paragraph('Extended Code 39', styleN))
|
||||
storyAdd(Extended39("A012345B}"))
|
||||
|
||||
storyAdd(Paragraph('Code93', styleN))
|
||||
storyAdd(Standard93("CODE 93"))
|
||||
|
||||
storyAdd(Paragraph('Extended Code93', styleN))
|
||||
storyAdd(Extended93("L@@K! Code 93 :-)")) #, barWidth=0.005 * inch))
|
||||
|
||||
storyAdd(Paragraph('Code 128', styleN))
|
||||
storyAdd(Code128("AB-12345678"))
|
||||
|
||||
storyAdd(Paragraph('Code 128 Auto', styleN))
|
||||
storyAdd(Code128Auto("AB-12345678"))
|
||||
|
||||
storyAdd(Paragraph('USPS FIM', styleN))
|
||||
storyAdd(FIM("A"))
|
||||
|
||||
storyAdd(Paragraph('USPS POSTNET', styleN))
|
||||
storyAdd(POSTNET('78247-1043'))
|
||||
|
||||
storyAdd(Paragraph('USPS 4 State', styleN))
|
||||
storyAdd(USPS_4State('01234567094987654321','01234567891'))
|
||||
|
||||
from reportlab.graphics.barcode import createBarcodeDrawing
|
||||
|
||||
storyAdd(Paragraph('EAN13', styleN))
|
||||
storyAdd(createBarcodeDrawing('EAN13', value='123456789012'))
|
||||
|
||||
storyAdd(Paragraph('EAN13 quiet=False', styleN))
|
||||
storyAdd(createBarcodeDrawing('EAN13', value='123456789012', quiet=False))
|
||||
|
||||
storyAdd(Paragraph('EAN8', styleN))
|
||||
storyAdd(createBarcodeDrawing('EAN8', value='1234567'))
|
||||
|
||||
storyAdd(PageBreak())
|
||||
|
||||
storyAdd(Paragraph('EAN5 price=True', styleN))
|
||||
storyAdd(createBarcodeDrawing('EAN5', value='11299', price=True))
|
||||
|
||||
storyAdd(Paragraph('EAN5 price=True quiet=False', styleN))
|
||||
storyAdd(createBarcodeDrawing('EAN5', value='11299', price=True, quiet=False))
|
||||
|
||||
storyAdd(Paragraph('EAN5 price=False', styleN))
|
||||
storyAdd(createBarcodeDrawing('EAN5', value='11299', price=False))
|
||||
|
||||
storyAdd(Paragraph('ISBN alone', styleN))
|
||||
storyAdd(createBarcodeDrawing('ISBN', value='9781565924796'))
|
||||
|
||||
storyAdd(Paragraph('ISBN with ean5 price', styleN))
|
||||
storyAdd(createBarcodeDrawing('ISBN', value='9781565924796',price='01299'))
|
||||
|
||||
storyAdd(Paragraph('ISBN with ean5 price, quiet=False', styleN))
|
||||
storyAdd(createBarcodeDrawing('ISBN', value='9781565924796',price='01299',quiet=False))
|
||||
|
||||
storyAdd(Paragraph('UPCA', styleN))
|
||||
storyAdd(createBarcodeDrawing('UPCA', value='03600029145'))
|
||||
|
||||
storyAdd(Paragraph('USPS_4State', styleN))
|
||||
storyAdd(createBarcodeDrawing('USPS_4State', value='01234567094987654321',routing='01234567891'))
|
||||
|
||||
storyAdd(Paragraph('QR', styleN))
|
||||
storyAdd(createBarcodeDrawing('QR', value='01234567094987654321'))
|
||||
|
||||
storyAdd(Paragraph('QR', styleN))
|
||||
storyAdd(createBarcodeDrawing('QR', value='01234567094987654321',x=30,y=50))
|
||||
|
||||
def addCross(d,x,y,w=5,h=5, strokeColor='black', strokeWidth=0.5):
|
||||
w *= 0.5
|
||||
h *= 0.5
|
||||
d.add(Line(x-w,y,x+w,y,strokeWidth=0.5,strokeColor=colors.blue))
|
||||
d.add(Line(x, y-h, x, y+h,strokeWidth=0.5,strokeColor=colors.blue))
|
||||
storyAdd(Paragraph('QR in drawing at (0,0)', styleN))
|
||||
d = Drawing(100,100)
|
||||
d.add(Rect(0,0,100,100,strokeWidth=1,strokeColor=colors.red,fillColor=None))
|
||||
d.add(QrCodeWidget(value='01234567094987654321'))
|
||||
storyAdd(d)
|
||||
|
||||
storyAdd(Paragraph('QR in drawing at (10,10)', styleN))
|
||||
d = Drawing(100,100)
|
||||
d.add(Rect(0,0,100,100,strokeWidth=1,strokeColor=colors.red,fillColor=None))
|
||||
addCross(d,10,10)
|
||||
d.add(QrCodeWidget(value='01234567094987654321',x=10,y=10))
|
||||
storyAdd(d)
|
||||
|
||||
storyAdd(Paragraph('Label Size', styleN))
|
||||
storyAdd(XBox((2.0 + 5.0/8.0)*inch, 1 * inch, '1x2-5/8"'))
|
||||
|
||||
storyAdd(Paragraph('Label Size', styleN))
|
||||
storyAdd(XBox((1.75)*inch, .5 * inch, '1/2x1-3/4"'))
|
||||
|
||||
if pylibdmtx:
|
||||
storyAdd(PageBreak())
|
||||
storyAdd(Paragraph('DataMatrix in drawing at (10,10)', styleN))
|
||||
d = Drawing(100,100)
|
||||
d.add(Rect(0,0,100,100,strokeWidth=1,strokeColor=colors.red,fillColor=None))
|
||||
addCross(d,10,10)
|
||||
d.add(DataMatrixWidget(value='1234567890',x=10,y=10))
|
||||
storyAdd(d)
|
||||
storyAdd(Paragraph('DataMatrix in drawing at (10,10)', styleN))
|
||||
d = Drawing(100,100)
|
||||
d.add(Rect(0,0,100,100,strokeWidth=1,strokeColor=colors.red,fillColor=None))
|
||||
addCross(d,10,10)
|
||||
d.add(DataMatrixWidget(value='1234567890',x=10,y=10,color='black',bgColor='lime'))
|
||||
storyAdd(d)
|
||||
|
||||
storyAdd(Paragraph('DataMatrix in drawing at (90,90) anchor=ne', styleN))
|
||||
d = Drawing(100,100)
|
||||
d.add(Rect(0,0,100,100,strokeWidth=1,strokeColor=colors.red,fillColor=None))
|
||||
addCross(d,90,90)
|
||||
d.add(DataMatrixWidget(value='1234567890',x=90,y=90,color='darkblue',bgColor='yellow', anchor='ne'))
|
||||
storyAdd(d)
|
||||
|
||||
|
||||
SimpleDocTemplate('out.pdf').build(story)
|
||||
print('saved out.pdf')
|
||||
|
||||
def fullTest(fileName="test_full.pdf"):
|
||||
"""Creates large-ish test document with a variety of parameters"""
|
||||
|
||||
story = []
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
styleN = styles['Normal']
|
||||
styleH = styles['Heading1']
|
||||
styleH2 = styles['Heading2']
|
||||
story = []
|
||||
|
||||
story.append(Paragraph('ReportLab %s Barcode Test Suite - full output' % __RL_Version__,styleH))
|
||||
story.append(Paragraph('Generated at %s' % time.ctime(time.time()), styleN))
|
||||
|
||||
story.append(Paragraph('About this document', styleH2))
|
||||
story.append(Paragraph('History and Status', styleH2))
|
||||
|
||||
story.append(Paragraph("""
|
||||
This is the test suite and docoumentation for the ReportLab open source barcode API.
|
||||
""", styleN))
|
||||
|
||||
story.append(Paragraph("""
|
||||
Several years ago Ty Sarna contributed a barcode module to the ReportLab community.
|
||||
Several of the codes were used by him in hiw work and to the best of our knowledge
|
||||
this was correct. These were written as flowable objects and were available in PDFs,
|
||||
but not in our graphics framework. However, we had no knowledge of barcodes ourselves
|
||||
and did not advertise or extend the package.
|
||||
""", styleN))
|
||||
|
||||
story.append(Paragraph("""
|
||||
We "wrapped" the barcodes to be usable within our graphics framework; they are now available
|
||||
as Drawing objects which can be rendered to EPS files or bitmaps. For the last 2 years this
|
||||
has been available in our Diagra and Report Markup Language products. However, we did not
|
||||
charge separately and use was on an "as is" basis.
|
||||
""", styleN))
|
||||
|
||||
story.append(Paragraph("""
|
||||
A major licensee of our technology has kindly agreed to part-fund proper productisation
|
||||
of this code on an open source basis in Q1 2006. This has involved addition of EAN codes
|
||||
as well as a proper testing program. Henceforth we intend to publicise the code more widely,
|
||||
gather feedback, accept contributions of code and treat it as "supported".
|
||||
""", styleN))
|
||||
|
||||
story.append(Paragraph("""
|
||||
This involved making available both downloads and testing resources. This PDF document
|
||||
is the output of the current test suite. It contains codes you can scan (if you use a nice sharp
|
||||
laser printer!), and will be extended over coming weeks to include usage examples and notes on
|
||||
each barcode and how widely tested they are. This is being done through documentation strings in
|
||||
the barcode objects themselves so should always be up to date.
|
||||
""", styleN))
|
||||
|
||||
story.append(Paragraph('Usage examples', styleH2))
|
||||
story.append(Paragraph("""
|
||||
To be completed
|
||||
""", styleN))
|
||||
|
||||
story.append(Paragraph('The codes', styleH2))
|
||||
story.append(Paragraph("""
|
||||
Below we show a scannable code from each barcode, with and without human-readable text.
|
||||
These are magnified about 2x from the natural size done by the original author to aid
|
||||
inspection. This will be expanded to include several test cases per code, and to add
|
||||
explanations of checksums. Be aware that (a) if you enter numeric codes which are too
|
||||
short they may be prefixed for you (e.g. "123" for an 8-digit code becomes "00000123"),
|
||||
and that the scanned results and readable text will generally include extra checksums
|
||||
at the end.
|
||||
""", styleN))
|
||||
|
||||
codeNames = getCodeNames()
|
||||
from reportlab.lib.utils import flatten
|
||||
width = [float(x[8:]) for x in sys.argv if x.startswith('--width=')]
|
||||
height = [float(x[9:]) for x in sys.argv if x.startswith('--height=')]
|
||||
isoScale = [int(x[11:]) for x in sys.argv if x.startswith('--isoscale=')]
|
||||
options = {}
|
||||
if width: options['width'] = width[0]
|
||||
if height: options['height'] = height[0]
|
||||
if isoScale: options['isoScale'] = isoScale[0]
|
||||
scales = [x[8:].split(',') for x in sys.argv if x.startswith('--scale=')]
|
||||
scales = list(map(float,scales and flatten(scales) or [1]))
|
||||
scales = list(map(float,scales and flatten(scales) or [1]))
|
||||
for scale in scales:
|
||||
story.append(PageBreak())
|
||||
story.append(Paragraph('Scale = %.1f'%scale, styleH2))
|
||||
story.append(Spacer(36, 12))
|
||||
for codeName in codeNames:
|
||||
s = [Paragraph('Code: ' + codeName, styleH2)]
|
||||
for hr in (0,1):
|
||||
s.append(Spacer(36, 12))
|
||||
dr = createBarcodeDrawing(codeName, humanReadable=hr,**options)
|
||||
dr.renderScale = scale
|
||||
s.append(dr)
|
||||
s.append(Spacer(36, 12))
|
||||
s.append(Paragraph('Barcode should say: ' + dr._bc.value, styleN))
|
||||
story.append(KeepTogether(s))
|
||||
|
||||
SimpleDocTemplate(fileName).build(story)
|
||||
print('created', fileName)
|
||||
|
||||
if __name__=='__main__':
|
||||
run()
|
||||
fullTest()
|
||||
def createSample(name,memory):
|
||||
f = open(name,'wb')
|
||||
f.write(memory)
|
||||
f.close()
|
||||
createSample('test_cbcim.png',createBarcodeImageInMemory('EAN13', value='123456789012'))
|
||||
createSample('test_cbcim.gif',createBarcodeImageInMemory('EAN8', value='1234567', format='gif'))
|
||||
createSample('test_cbcim.pdf',createBarcodeImageInMemory('UPCA', value='03600029145',format='pdf', barHeight=40))
|
||||
createSample('test_cbcim.tiff',createBarcodeImageInMemory('USPS_4State', value='01234567094987654321',routing='01234567891',format='tiff'))
|
||||
createSample('test_cbcim-1.pdf',createBarcodeImageInMemory('QR',
|
||||
value='This is the end my only friend the end at the end of the Universe.',format='pdf'))
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#
|
||||
# Copyright (c) 1996-2000 Tyler C. Sarna <tsarna@sarna.org>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. All advertising materials mentioning features or use of this software
|
||||
# must display the following acknowledgement:
|
||||
# This product includes software developed by Tyler C. Sarna.
|
||||
# 4. Neither the name of the author nor the names of contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
|
||||
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.graphics.barcode.common import Barcode
|
||||
from string import digits as string_digits, whitespace as string_whitespace
|
||||
from reportlab.lib.utils import asNative
|
||||
|
||||
_fim_patterns = {
|
||||
'A' : "|| | ||",
|
||||
'B' : "| || || |",
|
||||
'C' : "|| | | ||",
|
||||
'D' : "||| | |||",
|
||||
# XXX There is an E.
|
||||
# The below has been seen, but dunno if it is E or not:
|
||||
# 'E' : '|||| ||||'
|
||||
}
|
||||
|
||||
_postnet_patterns = {
|
||||
'1' : "...||", '2' : "..|.|", '3' : "..||.", '4' : ".|..|",
|
||||
'5' : ".|.|.", '6' : ".||..", '7' : "|...|", '8' : "|..|.",
|
||||
'9' : "|.|..", '0' : "||...", 'S' : "|",
|
||||
}
|
||||
|
||||
class FIM(Barcode):
|
||||
"""
|
||||
FIM (Facing ID Marks) encode only one letter.
|
||||
There are currently four defined:
|
||||
|
||||
A Courtesy reply mail with pre-printed POSTNET
|
||||
B Business reply mail without pre-printed POSTNET
|
||||
C Business reply mail with pre-printed POSTNET
|
||||
D OCR Readable mail without pre-printed POSTNET
|
||||
|
||||
Options that may be passed to constructor:
|
||||
|
||||
value (single character string from the set A - D. required.):
|
||||
The value to encode.
|
||||
|
||||
quiet (bool, default 0):
|
||||
Whether to include quiet zones in the symbol.
|
||||
|
||||
The following may also be passed, but doing so will generate nonstandard
|
||||
symbols which should not be used. This is mainly documented here to
|
||||
show the defaults:
|
||||
|
||||
barHeight (float, default 5/8 inch):
|
||||
Height of the code. This might legitimately be overriden to make
|
||||
a taller symbol that will 'bleed' off the edge of the paper,
|
||||
leaving 5/8 inch remaining.
|
||||
|
||||
lquiet (float, default 1/4 inch):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or .15 times the symbol's
|
||||
length.
|
||||
|
||||
rquiet (float, default 15/32 inch):
|
||||
Quiet zone size to right left of code, if quiet is true.
|
||||
|
||||
Sources of information on FIM:
|
||||
|
||||
USPS Publication 25, A Guide to Business Mail Preparation
|
||||
http://new.usps.com/cpim/ftp/pubs/pub25.pdf
|
||||
"""
|
||||
barWidth = inch * (1.0/32.0)
|
||||
spaceWidth = inch * (1.0/16.0)
|
||||
barHeight = inch * (5.0/8.0)
|
||||
rquiet = inch * (0.25)
|
||||
lquiet = inch * (15.0/32.0)
|
||||
quiet = 0
|
||||
def __init__(self, value='', **args):
|
||||
value = str(value) if isinstance(value,int) else asNative(value)
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
Barcode.__init__(self, value)
|
||||
|
||||
def validate(self):
|
||||
self.valid = 1
|
||||
self.validated = ''
|
||||
for c in self.value:
|
||||
if c in string_whitespace:
|
||||
continue
|
||||
elif c in "abcdABCD":
|
||||
self.validated = self.validated + c.upper()
|
||||
else:
|
||||
self.valid = 0
|
||||
|
||||
if len(self.validated) != 1:
|
||||
raise ValueError("Input must be exactly one character")
|
||||
|
||||
return self.validated
|
||||
|
||||
def decompose(self):
|
||||
self.decomposed = ''
|
||||
for c in self.encoded:
|
||||
self.decomposed = self.decomposed + _fim_patterns[c]
|
||||
|
||||
return self.decomposed
|
||||
|
||||
def computeSize(self):
|
||||
self._width = (len(self.decomposed) - 1) * self.spaceWidth + self.barWidth
|
||||
if self.quiet:
|
||||
self._width += self.lquiet + self.rquiet
|
||||
self._height = self.barHeight
|
||||
|
||||
def draw(self):
|
||||
self._calculate()
|
||||
left = self.quiet and self.lquiet or 0
|
||||
for c in self.decomposed:
|
||||
if c == '|':
|
||||
self.rect(left, 0.0, self.barWidth, self.barHeight)
|
||||
left += self.spaceWidth
|
||||
self.drawHumanReadable()
|
||||
|
||||
def _humanText(self):
|
||||
return self.value
|
||||
|
||||
class POSTNET(Barcode):
|
||||
"""
|
||||
POSTNET is used in the US to encode "zip codes" (postal codes) on
|
||||
mail. It can encode 5, 9, or 11 digit codes. I've read that it's
|
||||
pointless to do 5 digits, since USPS will just have to re-print
|
||||
them with 9 or 11 digits.
|
||||
|
||||
Sources of information on POSTNET:
|
||||
|
||||
USPS Publication 25, A Guide to Business Mail Preparation
|
||||
http://new.usps.com/cpim/ftp/pubs/pub25.pdf
|
||||
"""
|
||||
quiet = 0
|
||||
shortHeight = inch * 0.050
|
||||
barHeight = inch * 0.125
|
||||
barWidth = inch * 0.018
|
||||
spaceWidth = inch * 0.0275
|
||||
def __init__(self, value='', **args):
|
||||
value = str(value) if isinstance(value,int) else asNative(value)
|
||||
for k, v in args.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
Barcode.__init__(self, value)
|
||||
|
||||
def validate(self):
|
||||
self.validated = ''
|
||||
self.valid = 1
|
||||
count = 0
|
||||
for c in self.value:
|
||||
if c in (string_whitespace + '-'):
|
||||
pass
|
||||
elif c in string_digits:
|
||||
count = count + 1
|
||||
if count == 6:
|
||||
self.validated = self.validated + '-'
|
||||
self.validated = self.validated + c
|
||||
else:
|
||||
self.valid = 0
|
||||
|
||||
if len(self.validated) not in [5, 10, 12]:
|
||||
self.valid = 0
|
||||
|
||||
return self.validated
|
||||
|
||||
def encode(self):
|
||||
self.encoded = "S"
|
||||
check = 0
|
||||
for c in self.validated:
|
||||
if c in string_digits:
|
||||
self.encoded = self.encoded + c
|
||||
check = check + int(c)
|
||||
elif c == '-':
|
||||
pass
|
||||
else:
|
||||
raise ValueError("Invalid character in input")
|
||||
check = (10 - check) % 10
|
||||
self.encoded = self.encoded + repr(check) + 'S'
|
||||
return self.encoded
|
||||
|
||||
def decompose(self):
|
||||
self.decomposed = ''
|
||||
for c in self.encoded:
|
||||
self.decomposed = self.decomposed + _postnet_patterns[c]
|
||||
return self.decomposed
|
||||
|
||||
def computeSize(self):
|
||||
self._width = len(self.decomposed) * self.barWidth + (len(self.decomposed) - 1) * self.spaceWidth
|
||||
self._height = self.barHeight
|
||||
|
||||
def draw(self):
|
||||
self._calculate()
|
||||
sdown = self.barHeight - self.shortHeight
|
||||
left = 0
|
||||
|
||||
for c in self.decomposed:
|
||||
if c == '.':
|
||||
h = self.shortHeight
|
||||
else:
|
||||
h = self.barHeight
|
||||
self.rect(left, 0.0, self.barWidth, h)
|
||||
left = left + self.barWidth + self.spaceWidth
|
||||
self.drawHumanReadable()
|
||||
|
||||
def _humanText(self):
|
||||
return self.encoded[1:-1]
|
||||
@@ -0,0 +1,465 @@
|
||||
#copyright ReportLab Inc. 2000-2016
|
||||
#see license.txt for license details
|
||||
from __future__ import print_function
|
||||
__version__='3.3.0'
|
||||
__all__ = ('USPS_4State',)
|
||||
|
||||
from reportlab.graphics.barcode.common import Barcode
|
||||
from reportlab.lib.utils import asNative
|
||||
|
||||
def nhex(i):
|
||||
'normalized hex'
|
||||
r = hex(i)
|
||||
r = r[:2]+r[2:].lower()
|
||||
if r.endswith('l'): r = r[:-1]
|
||||
return r
|
||||
|
||||
class USPS_4State(Barcode):
|
||||
''' USPS 4-State OneView (TM) barcode. All info from USPS-B-3200A
|
||||
'''
|
||||
_widthSize = 1
|
||||
_heightSize = 1
|
||||
_fontSize = 11
|
||||
_humanReadable = 0
|
||||
if True:
|
||||
tops = dict(
|
||||
F = (0.0625,0.0825),
|
||||
T = (0.0195,0.0285),
|
||||
A = (0.0625,0.0825),
|
||||
D = (0.0195,0.0285),
|
||||
)
|
||||
bottoms = dict(
|
||||
F = (-0.0625,-0.0825),
|
||||
T = (-0.0195,-0.0285),
|
||||
D = (-0.0625,-0.0825),
|
||||
A = (-0.0195,-0.0285),
|
||||
)
|
||||
dimensions = dict(
|
||||
width = (0.015, 0.025),
|
||||
pitch = (0.0416, 0.050),
|
||||
hcz = (0.125,0.125),
|
||||
vcz = (0.028,0.028),
|
||||
)
|
||||
else:
|
||||
tops = dict(
|
||||
F = (0.067,0.115),
|
||||
T = (0.021,0.040),
|
||||
A = (0.067,0.115),
|
||||
D = (0.021,0.040),
|
||||
)
|
||||
bottoms = dict(
|
||||
F = (-0.067,-0.115),
|
||||
D = (-0.067,-0.115),
|
||||
T = (-0.021,-0.040),
|
||||
A = (-0.021,-0.040),
|
||||
)
|
||||
dimensions = dict(
|
||||
width = (0.015, 0.025),
|
||||
pitch = (0.0416,0.050),
|
||||
hcz = (0.125,0.125),
|
||||
vcz = (0.040,0.040),
|
||||
)
|
||||
|
||||
def __init__(self,value='01234567094987654321',routing='',**kwd):
|
||||
self._init()
|
||||
value = str(value) if isinstance(value,int) else asNative(value)
|
||||
if not routing:
|
||||
#legal values for combined tracking + routing
|
||||
if len(value) in (20,25,29,31):
|
||||
value, routing = value[:20], value[20:]
|
||||
else:
|
||||
raise ValueError('value+routing length must be 20, 25, 29 or 31 digits not %d' % len(value))
|
||||
elif len(routing) not in (5,9,11):
|
||||
raise ValueError('routing length must be 5, 9 or 11 digits not %d' % len(routing))
|
||||
self._tracking = value
|
||||
self._routing = routing
|
||||
self._setKeywords(**kwd)
|
||||
|
||||
def _init(self):
|
||||
self._bvalue = None
|
||||
self._codewords = None
|
||||
self._characters = None
|
||||
self._barcodes = None
|
||||
|
||||
def scale(kind,D,s):
|
||||
V = D[kind]
|
||||
return 72*(V[0]*(1-s)+s*V[1])
|
||||
scale = staticmethod(scale)
|
||||
|
||||
def tracking(self,tracking):
|
||||
self._init()
|
||||
self._tracking = tracking
|
||||
tracking = property(lambda self: self._tracking,tracking)
|
||||
|
||||
def routing(self,routing):
|
||||
self._init()
|
||||
self._routing = routing
|
||||
routing = property(lambda self: self._routing,routing)
|
||||
|
||||
def widthSize(self,value):
|
||||
self._sized = None
|
||||
self._widthSize = min(max(0,value),1)
|
||||
widthSize = property(lambda self: self._widthSize,widthSize)
|
||||
|
||||
def heightSize(self,value):
|
||||
self._sized = None
|
||||
self._heightSize = value
|
||||
heightSize = property(lambda self: self._heightSize,heightSize)
|
||||
|
||||
def fontSize(self,value):
|
||||
self._sized = None
|
||||
self._fontSize = value
|
||||
fontSize = property(lambda self: self._fontSize,fontSize)
|
||||
|
||||
def humanReadable(self,value):
|
||||
self._sized = None
|
||||
self._humanReadable = value
|
||||
humanReadable = property(lambda self: self._humanReadable,humanReadable)
|
||||
|
||||
def binary(self):
|
||||
'''convert the 4 state string values to binary
|
||||
>>> print(nhex(USPS_4State('01234567094987654321','').binary))
|
||||
0x1122103b5c2004b1
|
||||
>>> print(nhex(USPS_4State('01234567094987654321','01234').binary))
|
||||
0xd138a87bab5cf3804b1
|
||||
>>> print(nhex(USPS_4State('01234567094987654321','012345678').binary))
|
||||
0x202bdc097711204d21804b1
|
||||
>>> print(nhex(USPS_4State('01234567094987654321','01234567891').binary))
|
||||
0x16907b2a24abc16a2e5c004b1
|
||||
'''
|
||||
value = self._bvalue
|
||||
if not value:
|
||||
routing = self.routing
|
||||
n = len(routing)
|
||||
try:
|
||||
if n==0:
|
||||
value = 0
|
||||
elif n==5:
|
||||
value = int(routing)+1
|
||||
elif n==9:
|
||||
value = int(routing)+100001
|
||||
elif n==11:
|
||||
value = int(routing)+1000100001
|
||||
else:
|
||||
raise ValueError
|
||||
except:
|
||||
raise ValueError('Problem converting %s, routing code must be 0, 5, 9 or 11 digits' % routing)
|
||||
|
||||
tracking = self.tracking
|
||||
svalue = tracking[0:2]
|
||||
try:
|
||||
value *= 10
|
||||
value += int(svalue[0])
|
||||
value *= 5
|
||||
value += int(svalue[1])
|
||||
except:
|
||||
raise ValueError('Problem converting %s, barcode identifier must be 2 digits' % svalue)
|
||||
|
||||
i = 2
|
||||
for name,nd in (('special services',3), ('customer identifier',6), ('sequence number',9)):
|
||||
j = i
|
||||
i += nd
|
||||
svalue = tracking[j:i]
|
||||
try:
|
||||
if len(svalue)!=nd: raise ValueError
|
||||
for j in range(nd):
|
||||
value *= 10
|
||||
value += int(svalue[j])
|
||||
except:
|
||||
raise ValueError('Problem converting %s, %s must be %d digits' % (svalue,name,nd))
|
||||
self._bvalue = value
|
||||
return value
|
||||
binary = property(binary)
|
||||
|
||||
def codewords(self):
|
||||
'''convert binary value into codewords
|
||||
>>> print(USPS_4State('01234567094987654321','01234567891').codewords)
|
||||
(673, 787, 607, 1022, 861, 19, 816, 1294, 35, 602)
|
||||
'''
|
||||
if not self._codewords:
|
||||
value = self.binary
|
||||
A, J = divmod(value,636)
|
||||
A, I = divmod(A,1365)
|
||||
A, H = divmod(A,1365)
|
||||
A, G = divmod(A,1365)
|
||||
A, F = divmod(A,1365)
|
||||
A, E = divmod(A,1365)
|
||||
A, D = divmod(A,1365)
|
||||
A, C = divmod(A,1365)
|
||||
A, B = divmod(A,1365)
|
||||
assert 0<=A<=658, 'improper value %s passed to _2codewords A-->%s' % (hex(int(value)),A)
|
||||
self._fcs = _crc11(value)
|
||||
if self._fcs&1024: A += 659
|
||||
J *= 2
|
||||
self._codewords = tuple(map(int,(A,B,C,D,E,F,G,H,I,J)))
|
||||
return self._codewords
|
||||
codewords = property(codewords)
|
||||
|
||||
|
||||
def table1(self):
|
||||
self.__class__.table1 = _initNof13Table(5,1287)
|
||||
return self.__class__.table1
|
||||
table1 = property(table1)
|
||||
|
||||
def table2(self):
|
||||
self.__class__.table2 = _initNof13Table(2,78)
|
||||
return self.__class__.table2
|
||||
table2 = property(table2)
|
||||
|
||||
def characters(self):
|
||||
''' convert own codewords to characters
|
||||
>>> print(' '.join(hex(c)[2:] for c in USPS_4State('01234567094987654321','01234567891').characters))
|
||||
dcb 85c 8e4 b06 6dd 1740 17c6 1200 123f 1b2b
|
||||
'''
|
||||
if not self._characters:
|
||||
codewords = self.codewords
|
||||
fcs = self._fcs
|
||||
C = []
|
||||
aC = C.append
|
||||
table1 = self.table1
|
||||
table2 = self.table2
|
||||
for i in range(10):
|
||||
cw = codewords[i]
|
||||
if cw<=1286:
|
||||
c = table1[cw]
|
||||
else:
|
||||
c = table2[cw-1287]
|
||||
if (fcs>>i)&1:
|
||||
c = ~c & 0x1fff
|
||||
aC(c)
|
||||
self._characters = tuple(C)
|
||||
return self._characters
|
||||
characters = property(characters)
|
||||
|
||||
def barcodes(self):
|
||||
'''Get 4 state bar codes for current routing and tracking
|
||||
>>> print(USPS_4State('01234567094987654321','01234567891').barcodes)
|
||||
AADTFFDFTDADTAADAATFDTDDAAADDTDTTDAFADADDDTFFFDDTTTADFAAADFTDAADA
|
||||
'''
|
||||
if not self._barcodes:
|
||||
C = self.characters
|
||||
B = []
|
||||
aB = B.append
|
||||
bits2bars = self._bits2bars
|
||||
for dc,db,ac,ab in self.table4:
|
||||
aB(bits2bars[((C[dc]>>db)&1)+2*((C[ac]>>ab)&1)])
|
||||
self._barcodes = ''.join(B)
|
||||
return self._barcodes
|
||||
barcodes = property(barcodes)
|
||||
|
||||
table4 = ((7, 2, 4, 3), (1, 10, 0, 0), (9, 12, 2, 8), (5, 5, 6, 11),
|
||||
(8, 9, 3, 1), (0, 1, 5, 12), (2, 5, 1, 8), (4, 4, 9, 11),
|
||||
(6, 3, 8, 10), (3, 9, 7, 6), (5, 11, 1, 4), (8, 5, 2, 12),
|
||||
(9, 10, 0, 2), (7, 1, 6, 7), (3, 6, 4, 9), (0, 3, 8, 6),
|
||||
(6, 4, 2, 7), (1, 1, 9, 9), (7, 10, 5, 2), (4, 0, 3, 8),
|
||||
(6, 2, 0, 4), (8, 11, 1, 0), (9, 8, 3, 12), (2, 6, 7, 7),
|
||||
(5, 1, 4, 10), (1, 12, 6, 9), (7, 3, 8, 0), (5, 8, 9, 7),
|
||||
(4, 6, 2, 10), (3, 4, 0, 5), (8, 4, 5, 7), (7, 11, 1, 9),
|
||||
(6, 0, 9, 6), (0, 6, 4, 8), (2, 1, 3, 2), (5, 9, 8, 12),
|
||||
(4, 11, 6, 1), (9, 5, 7, 4), (3, 3, 1, 2), (0, 7, 2, 0),
|
||||
(1, 3, 4, 1), (6, 10, 3, 5), (8, 7, 9, 4), (2, 11, 5, 6),
|
||||
(0, 8, 7, 12), (4, 2, 8, 1), (5, 10, 3, 0), (9, 3, 0, 9),
|
||||
(6, 5, 2, 4), (7, 8, 1, 7), (5, 0, 4, 5), (2, 3, 0, 10),
|
||||
(6, 12, 9, 2), (3, 11, 1, 6), (8, 8, 7, 9), (5, 4, 0, 11),
|
||||
(1, 5, 2, 2), (9, 1, 4, 12), (8, 3, 6, 6), (7, 0, 3, 7),
|
||||
(4, 7, 7, 5), (0, 12, 1, 11), (2, 9, 9, 0), (6, 8, 5, 3),
|
||||
(3, 10, 8, 2))
|
||||
|
||||
_bits2bars = 'T','D','A','F'
|
||||
horizontalClearZone = property(lambda self: self.scale('hcz',self.dimensions,self.widthScale))
|
||||
verticalClearZone = property(lambda self: self.scale('vcz',self.dimensions,self.heightScale))
|
||||
|
||||
@property
|
||||
def barWidth(self):
|
||||
if '_barWidth' in self.__dict__:
|
||||
return self.__dict__['_barWidth']
|
||||
return self.scale('width',self.dimensions,self.widthScale)
|
||||
|
||||
@barWidth.setter
|
||||
def barWidth(self,value):
|
||||
n, x = self.dimensions['width']
|
||||
self.__dict__['_barWidth'] = 72*min(max(value/72.0,n),x)
|
||||
|
||||
@property
|
||||
def pitch(self):
|
||||
if '_pitch' in self.__dict__:
|
||||
return self.__dict__['_pitch']
|
||||
return self.scale('pitch',self.dimensions,self.widthScale)
|
||||
|
||||
@pitch.setter
|
||||
def pitch(self,value):
|
||||
n, x = self.dimensions['pitch']
|
||||
self.__dict__['_pitch'] = 72*min(max(value/72.0,n),x)
|
||||
|
||||
@property
|
||||
def barHeight(self):
|
||||
if '_barHeight' in self.__dict__:
|
||||
return self.__dict__['_barHeight']
|
||||
return self.scale('F',self.tops,self.heightScale) - self.scale('F',self.bottoms,self.heightScale)
|
||||
|
||||
@barHeight.setter
|
||||
def barHeight(self,value):
|
||||
n = self.tops['F'][0] - self.bottoms['F'][0]
|
||||
x = self.tops['F'][1] - self.bottoms['F'][1]
|
||||
value = self.__dict__['_barHeight'] = 72*min(max(value/72.0,n),x)
|
||||
self.heightSize = (value - n)/(x-n)
|
||||
|
||||
widthScale = property(lambda self: min(1,max(0,self.widthSize)))
|
||||
heightScale = property(lambda self: min(1,max(0,self.heightSize)))
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
self.computeSize()
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
self.computeSize()
|
||||
return self._height
|
||||
|
||||
#we ignore attempts to set the dimensions
|
||||
@width.setter
|
||||
def width(self,v):
|
||||
pass
|
||||
@height.setter
|
||||
def height(self,v):
|
||||
pass
|
||||
|
||||
def computeSize(self):
|
||||
if not getattr(self,'_sized',None):
|
||||
ws = self.widthScale
|
||||
hs = self.heightScale
|
||||
barHeight = self.barHeight
|
||||
barWidth = self.barWidth
|
||||
pitch = self.pitch
|
||||
hcz = self.horizontalClearZone
|
||||
vcz = self.verticalClearZone
|
||||
self._width = 2*hcz + barWidth + 64*pitch
|
||||
self._height = 2*vcz+barHeight
|
||||
if self.humanReadable:
|
||||
self._height += self.fontSize*1.2+vcz
|
||||
self._sized = True
|
||||
|
||||
def wrap(self,aW,aH):
|
||||
self.computeSize()
|
||||
return self.width, self.height
|
||||
|
||||
def _getBarVInfo(self,y0=0):
|
||||
vInfo = {}
|
||||
hs = self.heightScale
|
||||
for b in ('T','D','A','F'):
|
||||
y = self.scale(b,self.bottoms,hs)+y0
|
||||
vInfo[b] = y,self.scale(b,self.tops,hs)+y0 - y
|
||||
return vInfo
|
||||
|
||||
def draw(self):
|
||||
self.computeSize()
|
||||
hcz = self.horizontalClearZone
|
||||
vcz = self.verticalClearZone
|
||||
bw = self.barWidth
|
||||
x = hcz
|
||||
y0 = vcz+self.barHeight*0.5
|
||||
dw = self.pitch
|
||||
vInfo = self._getBarVInfo(y0)
|
||||
for b in self.barcodes:
|
||||
yb, hb = vInfo[b]
|
||||
self.rect(x,yb,bw,hb)
|
||||
x += dw
|
||||
self.drawHumanReadable()
|
||||
|
||||
def value(self):
|
||||
tracking = self.tracking
|
||||
routing = self.routing
|
||||
routing = routing and (routing,) or ()
|
||||
return ' '.join((tracking[0:2],tracking[2:5],tracking[5:11],tracking[11:])+routing)
|
||||
value = property(value,lambda self,value: self.__dict__.__setitem__('tracking',value))
|
||||
|
||||
def drawHumanReadable(self):
|
||||
if self.humanReadable:
|
||||
hcz = self.horizontalClearZone
|
||||
vcz = self.verticalClearZone
|
||||
fontName = self.fontName
|
||||
fontSize = self.fontSize
|
||||
y = self.barHeight+2*vcz+0.2*fontSize
|
||||
self.annotate(hcz,y,self.value,fontName,fontSize)
|
||||
|
||||
def annotate(self,x,y,text,fontName,fontSize,anchor='middle'):
|
||||
Barcode.annotate(self,x,y,text,fontName,fontSize,anchor='start')
|
||||
|
||||
def _crc11(value):
|
||||
'''
|
||||
>>> usps = [USPS_4State('01234567094987654321',x).binary for x in ('','01234','012345678','01234567891')]
|
||||
>>> print(' '.join(nhex(x) for x in usps))
|
||||
0x1122103b5c2004b1 0xd138a87bab5cf3804b1 0x202bdc097711204d21804b1 0x16907b2a24abc16a2e5c004b1
|
||||
>>> print(' '.join(nhex(_crc11(x)) for x in usps))
|
||||
0x51 0x65 0x606 0x751
|
||||
'''
|
||||
hexbytes = nhex(int(value))[2:]
|
||||
hexbytes = '0'*(26-len(hexbytes))+hexbytes
|
||||
gp = 0x0F35
|
||||
fcs = 0x07FF
|
||||
data = int(hexbytes[:2],16)<<5
|
||||
for b in range(2,8):
|
||||
if (fcs ^ data)&0x400:
|
||||
fcs = (fcs<<1)^gp
|
||||
else:
|
||||
fcs = fcs<<1
|
||||
fcs &= 0x7ff
|
||||
data <<= 1
|
||||
|
||||
for x in range(2,2*13,2):
|
||||
data = int(hexbytes[x:x+2],16)<<3
|
||||
for b in range(8):
|
||||
if (fcs ^ data)&0x400:
|
||||
fcs = (fcs<<1)^gp
|
||||
else:
|
||||
fcs = fcs<<1
|
||||
fcs &= 0x7ff
|
||||
data <<= 1
|
||||
return fcs
|
||||
|
||||
def _ru13(i):
|
||||
'''reverse unsigned 13 bit number
|
||||
>>> print(_ru13(7936), _ru13(31), _ru13(47), _ru13(7808))
|
||||
31 7936 7808 47
|
||||
'''
|
||||
r = 0
|
||||
for x in range(13):
|
||||
r <<= 1
|
||||
r |= i & 1
|
||||
i >>= 1
|
||||
return r
|
||||
|
||||
def _initNof13Table(N,lenT):
|
||||
'''create and return table of 13 bit values with N bits on
|
||||
>>> T = _initNof13Table(5,1287)
|
||||
>>> print(' '.join('T[%d]=%d' % (i, T[i]) for i in (0,1,2,3,4,1271,1272,1284,1285,1286)))
|
||||
T[0]=31 T[1]=7936 T[2]=47 T[3]=7808 T[4]=55 T[1271]=6275 T[1272]=6211 T[1284]=856 T[1285]=744 T[1286]=496
|
||||
'''
|
||||
T = lenT*[None]
|
||||
l = 0
|
||||
u = lenT-1
|
||||
for c in range(8192):
|
||||
bc = 0
|
||||
for b in range(13):
|
||||
bc += (c&(1<<b))!=0
|
||||
if bc!=N: continue
|
||||
r = _ru13(c)
|
||||
if r<c: continue #we already looked at this pair
|
||||
if r==c:
|
||||
T[u] = c
|
||||
u -= 1
|
||||
else:
|
||||
T[l] = c
|
||||
l += 1
|
||||
T[l] = r
|
||||
l += 1
|
||||
assert l==(u+1), 'u+1(%d)!=l(%d) for %d of 13 table' % (u+1,l,N)
|
||||
return T
|
||||
|
||||
def _test():
|
||||
import doctest
|
||||
return doctest.testmod()
|
||||
|
||||
if __name__ == "__main__":
|
||||
_test()
|
||||
@@ -0,0 +1,357 @@
|
||||
#copyright ReportLab Europe Limited. 2000-2016
|
||||
#see license.txt for license details
|
||||
__version__='3.3.0'
|
||||
__all__= (
|
||||
'BarcodeI2of5',
|
||||
'BarcodeCode128',
|
||||
'BarcodeStandard93',
|
||||
'BarcodeExtended93',
|
||||
'BarcodeStandard39',
|
||||
'BarcodeExtended39',
|
||||
'BarcodeMSI',
|
||||
'BarcodeCodabar',
|
||||
'BarcodeCode11',
|
||||
'BarcodeFIM',
|
||||
'BarcodePOSTNET',
|
||||
'BarcodeUSPS_4State',
|
||||
)
|
||||
|
||||
from reportlab.lib.validators import isInt, isNumber, isString, isColorOrNone, isBoolean, EitherOr, isNumberOrNone
|
||||
from reportlab.lib.attrmap import AttrMap, AttrMapValue
|
||||
from reportlab.lib.colors import black
|
||||
from reportlab.lib.utils import rl_exec
|
||||
from reportlab.graphics.shapes import Rect, Group, String
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
|
||||
'''
|
||||
#snippet
|
||||
|
||||
#first make your Drawing
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
d= Drawing(100,50)
|
||||
|
||||
#create and set up the widget
|
||||
from reportlab.graphics.barcode.widgets import BarcodeStandard93
|
||||
bc = BarcodeStandard93()
|
||||
bc.value = 'RGB-123456'
|
||||
|
||||
#add to the drawing and save
|
||||
d.add(bc)
|
||||
# d.save(formats=['gif','pict'],fnRoot='bc_sample')
|
||||
'''
|
||||
|
||||
class _BarcodeWidget(PlotArea):
|
||||
_attrMap = AttrMap(BASE=PlotArea,
|
||||
barStrokeColor = AttrMapValue(isColorOrNone, desc='Color of bar borders.'),
|
||||
barFillColor = AttrMapValue(isColorOrNone, desc='Color of bar interior areas.'),
|
||||
barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'),
|
||||
value = AttrMapValue(EitherOr((isString,isNumber)), desc='Value.'),
|
||||
textColor = AttrMapValue(isColorOrNone, desc='Color of human readable text.'),
|
||||
valid = AttrMapValue(isBoolean),
|
||||
validated = AttrMapValue(isString,desc="validated form of input"),
|
||||
encoded = AttrMapValue(None,desc="encoded form of input"),
|
||||
decomposed = AttrMapValue(isString,desc="decomposed form of input"),
|
||||
canv = AttrMapValue(None,desc="temporarily used for internal methods"),
|
||||
gap = AttrMapValue(isNumberOrNone, desc='Width of inter character gaps.'),
|
||||
)
|
||||
|
||||
textColor = barFillColor = black
|
||||
barStrokeColor = None
|
||||
barStrokeWidth = 0
|
||||
_BCC = None
|
||||
def __init__(self,_value='',**kw):
|
||||
PlotArea.__init__(self)
|
||||
if 'width' in self.__dict__: del self.__dict__['width']
|
||||
if 'height' in self.__dict__: del self.__dict__['height']
|
||||
self.x = self.y = 0
|
||||
kw.setdefault('value',_value)
|
||||
self._BCC.__init__(self,**kw)
|
||||
|
||||
def rect(self,x,y,w,h,**kw):
|
||||
#this allows the base code to draw rectangles for us using self.rect
|
||||
#using direct keyword argument overrides see eg common.py line 140 on
|
||||
for k,v in (('strokeColor',self.barStrokeColor),
|
||||
('strokeWidth',self.barStrokeWidth),
|
||||
('fillColor',self.barFillColor)):
|
||||
kw.setdefault(k,v)
|
||||
self._Gadd(Rect(self.x+x,self.y+y,w,h, **kw))
|
||||
|
||||
def draw(self):
|
||||
if not self._BCC: raise NotImplementedError("Abstract class %s cannot be drawn" % self.__class__.__name__)
|
||||
self.canv = self
|
||||
G = Group()
|
||||
self._Gadd = G.add
|
||||
self._Gadd(Rect(self.x,self.y,self.width,self.height,fillColor=None,strokeColor=None,strokeWidth=0.0001))
|
||||
self._BCC.draw(self)
|
||||
del self.canv, self._Gadd
|
||||
return G
|
||||
|
||||
def annotate(self,x,y,text,fontName,fontSize,anchor='middle'):
|
||||
self._Gadd(String(self.x+x,self.y+y,text,fontName=fontName,fontSize=fontSize,
|
||||
textAnchor=anchor,fillColor=self.textColor))
|
||||
|
||||
def _BCW(doc,codeName,attrMap,mod,value,**kwds):
|
||||
"""factory for Barcode Widgets"""
|
||||
_pre_init = kwds.pop('_pre_init','')
|
||||
_methods = kwds.pop('_methods','')
|
||||
name = 'Barcode'+codeName
|
||||
ns = vars().copy()
|
||||
code = 'from %s import %s' % (mod,codeName)
|
||||
rl_exec(code,ns)
|
||||
ns['_BarcodeWidget'] = _BarcodeWidget
|
||||
ns['doc'] = ("\n\t'''%s'''" % doc) if doc else ''
|
||||
code = '''class %(name)s(_BarcodeWidget,%(codeName)s):%(doc)s
|
||||
\t_BCC = %(codeName)s
|
||||
\tcodeName = %(codeName)r
|
||||
\tdef __init__(self,**kw):%(_pre_init)s
|
||||
\t\t_BarcodeWidget.__init__(self,%(value)r,**kw)%(_methods)s''' % ns
|
||||
rl_exec(code,ns)
|
||||
Klass = ns[name]
|
||||
if attrMap: Klass._attrMap = attrMap
|
||||
for k, v in kwds.items():
|
||||
setattr(Klass,k,v)
|
||||
return Klass
|
||||
|
||||
BarcodeI2of5 = _BCW(
|
||||
"""Interleaved 2 of 5 is used in distribution and warehouse industries.
|
||||
|
||||
It encodes an even-numbered sequence of numeric digits. There is an optional
|
||||
module 10 check digit; if including this, the total length must be odd so that
|
||||
it becomes even after including the check digit. Otherwise the length must be
|
||||
even. Since the check digit is optional, our library does not check it.
|
||||
""",
|
||||
"I2of5",
|
||||
AttrMap(BASE=_BarcodeWidget,
|
||||
barWidth = AttrMapValue(isNumber,'''(float, default .0075):
|
||||
X-Dimension, or width of the smallest element
|
||||
Minumum is .0075 inch (7.5 mils).'''),
|
||||
ratio = AttrMapValue(isNumber,'''(float, default 2.2):
|
||||
The ratio of wide elements to narrow elements.
|
||||
Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the
|
||||
barWidth is greater than 20 mils (.02 inch))'''),
|
||||
gap = AttrMapValue(isNumberOrNone,'''(float or None, default None):
|
||||
width of intercharacter gap. None means "use barWidth".'''),
|
||||
barHeight = AttrMapValue(isNumber,'''(float, see default below):
|
||||
Height of the symbol. Default is the height of the two
|
||||
bearer bars (if they exist) plus the greater of .25 inch
|
||||
or .15 times the symbol's length.'''),
|
||||
checksum = AttrMapValue(isBoolean,'''(bool, default 1):
|
||||
Whether to compute and include the check digit'''),
|
||||
bearers = AttrMapValue(isNumber,'''(float, in units of barWidth. default 3.0):
|
||||
Height of bearer bars (horizontal bars along the top and
|
||||
bottom of the barcode). Default is 3 x-dimensions.
|
||||
Set to zero for no bearer bars. (Bearer bars help detect
|
||||
misscans, so it is suggested to leave them on).'''),
|
||||
bearerBox = AttrMapValue(isBoolean,'''(bool, default 0):
|
||||
if True turn bearers into a box'''),
|
||||
quiet = AttrMapValue(isBoolean,'''(bool, default 1):
|
||||
Whether to include quiet zones in the symbol.'''),
|
||||
|
||||
lquiet = AttrMapValue(isNumber,'''(float, see default below):
|
||||
Quiet zone size to left of code, if quiet is true.
|
||||
Default is the greater of .25 inch, or .15 times the symbol's
|
||||
length.'''),
|
||||
|
||||
rquiet = AttrMapValue(isNumber,'''(float, defaults as above):
|
||||
Quiet zone size to right left of code, if quiet is true.'''),
|
||||
fontName = AttrMapValue(isString, desc='human readable font'),
|
||||
fontSize = AttrMapValue(isNumber, desc='human readable font size'),
|
||||
humanReadable = AttrMapValue(isBoolean, desc='if human readable'),
|
||||
stop = AttrMapValue(isBoolean, desc='if we use start/stop symbols (default 1)'),
|
||||
),
|
||||
'reportlab.graphics.barcode.common',
|
||||
1234,
|
||||
_tests = [
|
||||
'12',
|
||||
'1234',
|
||||
'123456',
|
||||
'12345678',
|
||||
'1234567890'
|
||||
],
|
||||
)
|
||||
|
||||
BarcodeCode128 = _BCW("""Code 128 encodes any number of characters in the ASCII character set.""",
|
||||
"Code128",
|
||||
AttrMap(BASE=BarcodeI2of5,UNWANTED=('bearers','checksum','ratio','checksum','stop')),
|
||||
'reportlab.graphics.barcode.code128',
|
||||
"AB-12345678",
|
||||
_tests = ['ReportLab Rocks!', 'PFWZF'],
|
||||
)
|
||||
|
||||
BarcodeCode128Auto = _BCW(
|
||||
'Modified Code128 to use auto encoding',
|
||||
'Code128Auto',
|
||||
AttrMap(BASE=BarcodeCode128),
|
||||
'reportlab.graphics.barcode.code128',
|
||||
'XY149740345GB'
|
||||
)
|
||||
|
||||
BarcodeStandard93=_BCW("""This is a compressed form of Code 39""",
|
||||
"Standard93",
|
||||
AttrMap(BASE=BarcodeCode128,
|
||||
stop = AttrMapValue(isBoolean, desc='if we use start/stop symbols (default 1)'),
|
||||
),
|
||||
'reportlab.graphics.barcode.code93',
|
||||
"CODE 93",
|
||||
)
|
||||
|
||||
BarcodeExtended93=_BCW("""This is a compressed form of Code 39, allowing the full ASCII charset""",
|
||||
"Extended93",
|
||||
AttrMap(BASE=BarcodeCode128,
|
||||
stop = AttrMapValue(isBoolean, desc='if we use start/stop symbols (default 1)'),
|
||||
),
|
||||
'reportlab.graphics.barcode.code93',
|
||||
"L@@K! Code 93 ;-)",
|
||||
)
|
||||
|
||||
BarcodeStandard39=_BCW("""Code39 is widely used in non-retail, especially US defence and health.
|
||||
Allowed characters are 0-9, A-Z (caps only), space, and -.$/+%*.""",
|
||||
"Standard39",
|
||||
AttrMap(BASE=BarcodeI2of5),
|
||||
'reportlab.graphics.barcode.code39',
|
||||
"A012345B%R",
|
||||
)
|
||||
|
||||
BarcodeExtended39=_BCW("""Extended 39 encodes the full ASCII character set by encoding
|
||||
characters as pairs of Code 39 characters; $, /, % and + are used as
|
||||
shift characters.""",
|
||||
"Extended39",
|
||||
AttrMap(BASE=BarcodeI2of5),
|
||||
'reportlab.graphics.barcode.code39',
|
||||
"A012345B}",
|
||||
)
|
||||
|
||||
BarcodeMSI=_BCW("""MSI is used for inventory control in retail applications.
|
||||
|
||||
There are several methods for calculating check digits so we
|
||||
do not implement one.
|
||||
""",
|
||||
"MSI",
|
||||
AttrMap(BASE=BarcodeI2of5),
|
||||
'reportlab.graphics.barcode.common',
|
||||
1234,
|
||||
)
|
||||
|
||||
BarcodeCodabar=_BCW("""Used in blood banks, photo labs and FedEx labels.
|
||||
Encodes 0-9, -$:/.+, and four start/stop characters A-D.""",
|
||||
"Codabar",
|
||||
AttrMap(BASE=BarcodeI2of5),
|
||||
'reportlab.graphics.barcode.common',
|
||||
"A012345B",
|
||||
)
|
||||
|
||||
BarcodeCode11=_BCW("""Used mostly for labelling telecommunications equipment.
|
||||
It encodes numeric digits.""",
|
||||
'Code11',
|
||||
AttrMap(BASE=BarcodeI2of5,
|
||||
checksum = AttrMapValue(isInt,'''(integer, default 2):
|
||||
Whether to compute and include the check digit(s).
|
||||
(0 none, 1 1-digit, 2 2-digit, -1 auto, default -1):
|
||||
How many checksum digits to include. -1 ("auto") means
|
||||
1 if the number of digits is 10 or less, else 2.'''),
|
||||
),
|
||||
'reportlab.graphics.barcode.common',
|
||||
"01234545634563",
|
||||
)
|
||||
|
||||
BarcodeFIM=_BCW("""
|
||||
FIM was developed as part of the POSTNET barcoding system.
|
||||
FIM (Face Identification Marking) is used by the cancelling machines
|
||||
to sort mail according to whether or not they have bar code
|
||||
and their postage requirements. There are four types of FIM
|
||||
called FIM A, FIM B, FIM C, and FIM D.
|
||||
|
||||
The four FIM types have the following meanings:
|
||||
FIM A- Postage required pre-barcoded
|
||||
FIM B - Postage pre-paid, no bar code exists
|
||||
FIM C- Postage prepaid prebarcoded
|
||||
FIM D- Postage required, no bar code exists""",
|
||||
"FIM",
|
||||
AttrMap(BASE=_BarcodeWidget,
|
||||
barWidth = AttrMapValue(isNumber,'''(float, default 1/32in): the bar width.'''),
|
||||
spaceWidth = AttrMapValue(isNumber,'''(float or None, default 1/16in):
|
||||
width of intercharacter gap. None means "use barWidth".'''),
|
||||
barHeight = AttrMapValue(isNumber,'''(float, default 5/8in): The bar height.'''),
|
||||
quiet = AttrMapValue(isBoolean,'''(bool, default 0):
|
||||
Whether to include quiet zones in the symbol.'''),
|
||||
lquiet = AttrMapValue(isNumber,'''(float, default: 15/32in):
|
||||
Quiet zone size to left of code, if quiet is true.'''),
|
||||
rquiet = AttrMapValue(isNumber,'''(float, default 1/4in):
|
||||
Quiet zone size to right left of code, if quiet is true.'''),
|
||||
fontName = AttrMapValue(isString, desc='human readable font'),
|
||||
fontSize = AttrMapValue(isNumber, desc='human readable font size'),
|
||||
humanReadable = AttrMapValue(isBoolean, desc='if human readable'),
|
||||
),
|
||||
'reportlab.graphics.barcode.usps',
|
||||
"A",
|
||||
)
|
||||
|
||||
BarcodePOSTNET=_BCW('',
|
||||
"POSTNET",
|
||||
AttrMap(BASE=_BarcodeWidget,
|
||||
barWidth = AttrMapValue(isNumber,'''(float, default 0.018*in): the bar width.'''),
|
||||
spaceWidth = AttrMapValue(isNumber,'''(float or None, default 0.0275in): width of intercharacter gap.'''),
|
||||
shortHeight = AttrMapValue(isNumber,'''(float, default 0.05in): The short bar height.'''),
|
||||
barHeight = AttrMapValue(isNumber,'''(float, default 0.125in): The full bar height.'''),
|
||||
fontName = AttrMapValue(isString, desc='human readable font'),
|
||||
fontSize = AttrMapValue(isNumber, desc='human readable font size'),
|
||||
humanReadable = AttrMapValue(isBoolean, desc='if human readable'),
|
||||
),
|
||||
'reportlab.graphics.barcode.usps',
|
||||
"78247-1043",
|
||||
)
|
||||
|
||||
BarcodeUSPS_4State=_BCW('',
|
||||
"USPS_4State",
|
||||
AttrMap(BASE=_BarcodeWidget,
|
||||
widthSize = AttrMapValue(isNumber,'''(float, default 1): the bar width size adjustment between 0 and 1.'''),
|
||||
heightSize = AttrMapValue(isNumber,'''(float, default 1): the bar height size adjustment between 0 and 1.'''),
|
||||
fontName = AttrMapValue(isString, desc='human readable font'),
|
||||
fontSize = AttrMapValue(isNumber, desc='human readable font size'),
|
||||
tracking = AttrMapValue(isString, desc='tracking data'),
|
||||
routing = AttrMapValue(isString, desc='routing data'),
|
||||
humanReadable = AttrMapValue(isBoolean, desc='if human readable'),
|
||||
barWidth = AttrMapValue(isNumber, desc='barWidth'),
|
||||
barHeight = AttrMapValue(isNumber, desc='barHeight'),
|
||||
pitch = AttrMapValue(isNumber, desc='pitch'),
|
||||
),
|
||||
'reportlab.graphics.barcode.usps4s',
|
||||
'01234567094987654321',
|
||||
_pre_init="\n\t\tkw.setdefault('routing','01234567891')\n",
|
||||
_methods = "\n\tdef annotate(self,x,y,text,fontName,fontSize,anchor='middle'):\n\t\t_BarcodeWidget.annotate(self,x,y,text,fontName,fontSize,anchor='start')\n"
|
||||
)
|
||||
BarcodeECC200DataMatrix = _BCW(
|
||||
'ECC200DataMatrix',
|
||||
'ECC200DataMatrix',
|
||||
AttrMap(BASE=_BarcodeWidget,
|
||||
x=AttrMapValue(isNumber, desc='X position of the lower-left corner of the barcode.'),
|
||||
y=AttrMapValue(isNumber, desc='Y position of the lower-left corner of the barcode.'),
|
||||
barWidth=AttrMapValue(isNumber, desc='Size of data modules.'),
|
||||
barFillColor=AttrMapValue(isColorOrNone, desc='Color of data modules.'),
|
||||
value=AttrMapValue(EitherOr((isString,isNumber)), desc='Value.'),
|
||||
height=AttrMapValue(None, desc='ignored'),
|
||||
width=AttrMapValue(None, desc='ignored'),
|
||||
strokeColor=AttrMapValue(None, desc='ignored'),
|
||||
strokeWidth=AttrMapValue(None, desc='ignored'),
|
||||
fillColor=AttrMapValue(None, desc='ignored'),
|
||||
background=AttrMapValue(None, desc='ignored'),
|
||||
debug=AttrMapValue(None, desc='ignored'),
|
||||
gap=AttrMapValue(None, desc='ignored'),
|
||||
row_modules=AttrMapValue(None, desc='???'),
|
||||
col_modules=AttrMapValue(None, desc='???'),
|
||||
row_regions=AttrMapValue(None, desc='???'),
|
||||
col_regions=AttrMapValue(None, desc='???'),
|
||||
cw_data=AttrMapValue(None, desc='???'),
|
||||
cw_ecc=AttrMapValue(None, desc='???'),
|
||||
row_usable_modules = AttrMapValue(None, desc='???'),
|
||||
col_usable_modules = AttrMapValue(None, desc='???'),
|
||||
valid = AttrMapValue(None, desc='???'),
|
||||
validated = AttrMapValue(None, desc='???'),
|
||||
decomposed = AttrMapValue(None, desc='???'),
|
||||
),
|
||||
'reportlab.graphics.barcode.ecc200datamatrix',
|
||||
'JGB 0204H20B012722900021AC35B2100001003241014241014TPS01 WJ067073605GB185 MOUNT PLEASANT MAIL CENTER EC1A1BB9ZGBREC1A1BB EC1A1BB STEST FILE FOR SPEC '
|
||||
)
|
||||
|
||||
if __name__=='__main__':
|
||||
raise ValueError('widgets.py has no script function')
|
||||
@@ -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/graphics/charts/__init__.py
|
||||
__version__='3.3.0'
|
||||
__doc__='''Business charts'''
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/areas.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__='''This module defines a Area mixin classes'''
|
||||
|
||||
from reportlab.lib.validators import isNumber, isColorOrNone, isNoneOrShape
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from reportlab.graphics.shapes import Rect, Group, Line, Polygon
|
||||
from reportlab.lib.attrmap import AttrMap, AttrMapValue
|
||||
from reportlab.lib.colors import grey
|
||||
|
||||
class PlotArea(Widget):
|
||||
"Abstract base class representing a chart's plot area, pretty unusable by itself."
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc='X position of the lower-left corner of the chart.'),
|
||||
y = AttrMapValue(isNumber, desc='Y position of the lower-left corner of the chart.'),
|
||||
width = AttrMapValue(isNumber, desc='Width of the chart.'),
|
||||
height = AttrMapValue(isNumber, desc='Height of the chart.'),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc='Color of the plot area border.'),
|
||||
strokeWidth = AttrMapValue(isNumber, desc='Width plot area border.'),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc='Color of the plot area interior.'),
|
||||
background = AttrMapValue(isNoneOrShape, desc='Handle to background object e.g. Rect(0,0,width,height).'),
|
||||
debug = AttrMapValue(isNumber, desc='Used only for debugging.'),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.x = 20
|
||||
self.y = 10
|
||||
self.height = 85
|
||||
self.width = 180
|
||||
self.strokeColor = None
|
||||
self.strokeWidth = 1
|
||||
self.fillColor = None
|
||||
self.background = None
|
||||
self.debug = 0
|
||||
|
||||
def makeBackground(self):
|
||||
if self.background is not None:
|
||||
BG = self.background
|
||||
if isinstance(BG,Group):
|
||||
g = BG
|
||||
for bg in g.contents:
|
||||
bg.x = self.x
|
||||
bg.y = self.y
|
||||
bg.width = self.width
|
||||
bg.height = self.height
|
||||
else:
|
||||
g = Group()
|
||||
if type(BG) not in (type(()),type([])): BG=(BG,)
|
||||
for bg in BG:
|
||||
bg.x = self.x
|
||||
bg.y = self.y
|
||||
bg.width = self.width
|
||||
bg.height = self.height
|
||||
g.add(bg)
|
||||
return g
|
||||
else:
|
||||
strokeColor,strokeWidth,fillColor=self.strokeColor, self.strokeWidth, self.fillColor
|
||||
if (strokeWidth and strokeColor) or fillColor:
|
||||
g = Group()
|
||||
_3d_dy = getattr(self,'_3d_dy',None)
|
||||
x = self.x
|
||||
y = self.y
|
||||
h = self.height
|
||||
w = self.width
|
||||
if _3d_dy is not None:
|
||||
_3d_dx = self._3d_dx
|
||||
if fillColor and not strokeColor:
|
||||
from reportlab.lib.colors import Blacker
|
||||
c = Blacker(fillColor, getattr(self,'_3d_blacken',0.7))
|
||||
else:
|
||||
c = strokeColor
|
||||
if not strokeWidth: strokeWidth = 0.5
|
||||
if fillColor or strokeColor or c:
|
||||
bg = Polygon([x,y,x,y+h,x+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y],
|
||||
strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fillColor)
|
||||
g.add(bg)
|
||||
g.add(Line(x,y,x+_3d_dx,y+_3d_dy, strokeWidth=0.5, strokeColor=c))
|
||||
g.add(Line(x+_3d_dx,y+_3d_dy, x+_3d_dx,y+h+_3d_dy,strokeWidth=0.5, strokeColor=c))
|
||||
fc = Blacker(c, getattr(self,'_3d_blacken',0.8))
|
||||
g.add(Polygon([x,y,x+_3d_dx,y+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y],
|
||||
strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fc))
|
||||
bg = Line(x+_3d_dx,y+_3d_dy, x+w+_3d_dx,y+_3d_dy,strokeWidth=0.5, strokeColor=c)
|
||||
else:
|
||||
bg = None
|
||||
else:
|
||||
bg = Rect(x, y, w, h,
|
||||
strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor)
|
||||
if bg: g.add(bg)
|
||||
return g
|
||||
else:
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
from reportlab.lib.colors import _PCMYK_black
|
||||
from reportlab.graphics.charts.textlabels import Label
|
||||
from reportlab.graphics.shapes import Circle, Drawing, Group, Line, Rect, String
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.lib.validators import *
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.pdfbase.pdfmetrics import getFont
|
||||
from reportlab.graphics.charts.lineplots import _maxWidth
|
||||
|
||||
class DotBox(Widget):
|
||||
"""Returns a dotbox widget."""
|
||||
|
||||
#Doesn't use TypedPropertyCollection for labels - this can be a later improvement
|
||||
_attrMap = AttrMap(
|
||||
xlabels = AttrMapValue(isNoneOrListOfNoneOrStrings,
|
||||
desc="List of text labels for boxes on left hand side"),
|
||||
ylabels = AttrMapValue(isNoneOrListOfNoneOrStrings,
|
||||
desc="Text label for second box on left hand side"),
|
||||
labelFontName = AttrMapValue(isString,
|
||||
desc="Name of font used for the labels"),
|
||||
labelFontSize = AttrMapValue(isNumber,
|
||||
desc="Size of font used for the labels"),
|
||||
labelOffset = AttrMapValue(isNumber,
|
||||
desc="Space between label text and grid edge"),
|
||||
strokeWidth = AttrMapValue(isNumber,
|
||||
desc='Width of the grid and dot outline'),
|
||||
gridDivWidth = AttrMapValue(isNumber,
|
||||
desc="Width of each 'box'"),
|
||||
gridColor = AttrMapValue(isColor,
|
||||
desc='Colour for the box and gridding'),
|
||||
dotDiameter = AttrMapValue(isNumber,
|
||||
desc="Diameter of the circle used for the 'dot'"),
|
||||
dotColor = AttrMapValue(isColor,
|
||||
desc='Colour of the circle on the box'),
|
||||
dotXPosition = AttrMapValue(isNumber,
|
||||
desc='X Position of the circle'),
|
||||
dotYPosition = AttrMapValue(isNumber,
|
||||
desc='X Position of the circle'),
|
||||
x = AttrMapValue(isNumber,
|
||||
desc='X Position of dotbox'),
|
||||
y = AttrMapValue(isNumber,
|
||||
desc='Y Position of dotbox'),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.xlabels=["Value", "Blend", "Growth"]
|
||||
self.ylabels=["Small", "Medium", "Large"]
|
||||
self.labelFontName = "Helvetica"
|
||||
self.labelFontSize = 6
|
||||
self.labelOffset = 5
|
||||
self.strokeWidth = 0.5
|
||||
self.gridDivWidth=0.5*cm
|
||||
self.gridColor=colors.Color(25/255.0,77/255.0,135/255.0)
|
||||
self.dotDiameter=0.4*cm
|
||||
self.dotColor=colors.Color(232/255.0,224/255.0,119/255.0)
|
||||
self.dotXPosition = 1
|
||||
self.dotYPosition = 1
|
||||
self.x = 30
|
||||
self.y = 5
|
||||
|
||||
|
||||
def _getDrawingDimensions(self):
|
||||
leftPadding=rightPadding=topPadding=bottomPadding=5
|
||||
#find width of grid
|
||||
tx=len(self.xlabels)*self.gridDivWidth
|
||||
#add padding (and offset)
|
||||
tx=tx+leftPadding+rightPadding+self.labelOffset
|
||||
#add in maximum width of text
|
||||
tx=tx+_maxWidth(self.xlabels, self.labelFontName, self.labelFontSize)
|
||||
#find height of grid
|
||||
ty=len(self.ylabels)*self.gridDivWidth
|
||||
#add padding (and offset)
|
||||
ty=ty+topPadding+bottomPadding+self.labelOffset
|
||||
#add in maximum width of text
|
||||
ty=ty+_maxWidth(self.ylabels, self.labelFontName, self.labelFontSize)
|
||||
#print (tx, ty)
|
||||
return (tx,ty)
|
||||
|
||||
def demo(self,drawing=None):
|
||||
if not drawing:
|
||||
tx,ty=self._getDrawingDimensions()
|
||||
drawing = Drawing(tx,ty)
|
||||
drawing.add(self.draw())
|
||||
return drawing
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
|
||||
#box
|
||||
g.add(Rect(self.x,self.y,len(self.xlabels)*self.gridDivWidth,len(self.ylabels)*self.gridDivWidth,
|
||||
strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
fillColor=None))
|
||||
|
||||
#internal gridding
|
||||
for f in range (1,len(self.ylabels)):
|
||||
#horizontal
|
||||
g.add(Line(strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
x1 = self.x,
|
||||
y1 = self.y+f*self.gridDivWidth,
|
||||
x2 = self.x+len(self.xlabels)*self.gridDivWidth,
|
||||
y2 = self.y+f*self.gridDivWidth))
|
||||
for f in range (1,len(self.xlabels)):
|
||||
#vertical
|
||||
g.add(Line(strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
x1 = self.x+f*self.gridDivWidth,
|
||||
y1 = self.y,
|
||||
x2 = self.x+f*self.gridDivWidth,
|
||||
y2 = self.y+len(self.ylabels)*self.gridDivWidth))
|
||||
|
||||
# draw the 'dot'
|
||||
g.add(Circle(strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
fillColor=self.dotColor,
|
||||
cx = self.x+(self.dotXPosition*self.gridDivWidth),
|
||||
cy = self.y+(self.dotYPosition*self.gridDivWidth),
|
||||
r = self.dotDiameter/2.0))
|
||||
|
||||
#used for centering y-labels (below)
|
||||
ascent=getFont(self.labelFontName).face.ascent
|
||||
if ascent==0:
|
||||
ascent=0.718 # default (from helvetica)
|
||||
ascent=ascent*self.labelFontSize # normalize
|
||||
|
||||
#do y-labels
|
||||
if self.ylabels != None:
|
||||
for f in range (len(self.ylabels)-1,-1,-1):
|
||||
if self.ylabels[f]!= None:
|
||||
g.add(String(strokeColor=self.gridColor,
|
||||
text = self.ylabels[f],
|
||||
fontName = self.labelFontName,
|
||||
fontSize = self.labelFontSize,
|
||||
fillColor=_PCMYK_black,
|
||||
x = self.x-self.labelOffset,
|
||||
y = self.y+(f*self.gridDivWidth+(self.gridDivWidth-ascent)/2.0),
|
||||
textAnchor = 'end'))
|
||||
|
||||
#do x-labels
|
||||
if self.xlabels != None:
|
||||
for f in range (0,len(self.xlabels)):
|
||||
if self.xlabels[f]!= None:
|
||||
l=Label()
|
||||
l.x=self.x+(f*self.gridDivWidth)+(self.gridDivWidth+ascent)/2.0
|
||||
l.y=self.y+(len(self.ylabels)*self.gridDivWidth)+self.labelOffset
|
||||
l.angle=90
|
||||
l.textAnchor='start'
|
||||
l.fontName = self.labelFontName
|
||||
l.fontSize = self.labelFontSize
|
||||
l.fillColor = _PCMYK_black
|
||||
l.setText(self.xlabels[f])
|
||||
l.boxAnchor = 'sw'
|
||||
l.draw()
|
||||
g.add(l)
|
||||
|
||||
return g
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
d = DotBox()
|
||||
d.demo().save(fnRoot="dotbox")
|
||||
@@ -0,0 +1,473 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/doughnut.py
|
||||
# doughnut chart
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""Doughnut chart
|
||||
|
||||
Produces a circular chart like the doughnut charts produced by Excel.
|
||||
Can handle multiple series (which produce concentric 'rings' in the chart).
|
||||
|
||||
"""
|
||||
|
||||
from math import sin, cos, pi
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isListOfStringsOrNone, OneOf,\
|
||||
isBoolean, isNumberOrNone, isListOfNoneOrNumber,\
|
||||
isListOfListOfNoneOrNumber, EitherOr, NoneOr, \
|
||||
isCallable
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.graphics.shapes import Group, Drawing, Wedge
|
||||
from reportlab.graphics.widgetbase import TypedPropertyCollection
|
||||
from reportlab.graphics.charts.piecharts import AbstractPieChart, WedgeProperties, _addWedgeLabel, fixLabelOverlaps
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
from functools import reduce
|
||||
|
||||
class SectorProperties(WedgeProperties):
|
||||
"""This holds descriptive information about the sectors in a doughnut chart.
|
||||
|
||||
It is not to be confused with the 'sector itself'; this just holds
|
||||
a recipe for how to format one, and does not allow you to hack the
|
||||
angles. It can format a genuine Sector object for you with its
|
||||
format method.
|
||||
"""
|
||||
_attrMap = AttrMap(BASE=WedgeProperties,
|
||||
)
|
||||
|
||||
class Doughnut(AbstractPieChart):
|
||||
_attrMap = AttrMap(BASE=AbstractPieChart,
|
||||
x = AttrMapValue(isNumber, desc='X position of the chart within its container.'),
|
||||
y = AttrMapValue(isNumber, desc='Y position of the chart within its container.'),
|
||||
width = AttrMapValue(isNumber, desc='width of doughnut bounding box. Need not be same as width.'),
|
||||
height = AttrMapValue(isNumber, desc='height of doughnut bounding box. Need not be same as height.'),
|
||||
data = AttrMapValue(EitherOr((isListOfNoneOrNumber,isListOfListOfNoneOrNumber)), desc='list of numbers defining sector sizes; need not sum to 1'),
|
||||
labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
|
||||
startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
|
||||
direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
|
||||
slices = AttrMapValue(None, desc="collection of sector descriptor objects"),
|
||||
simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"),
|
||||
# advanced usage
|
||||
checkLabelOverlap = AttrMapValue(isBoolean, desc="If true check and attempt to fix\n standard label overlaps(default off)",advancedUsage=1),
|
||||
sideLabels = AttrMapValue(isBoolean, desc="If true attempt to make chart with labels along side and pointers", advancedUsage=1),
|
||||
innerRadiusFraction = AttrMapValue(isNumberOrNone,
|
||||
desc='None or the fraction of the radius to be used as the inner hole.\nIf not a suitable default will be used.'),
|
||||
labelClass=AttrMapValue(NoneOr(isCallable), desc="A class factory to use for non simple labels"),
|
||||
angleRange = AttrMapValue(isNumber, desc='total degree range for the doughnut defaults to 360'),
|
||||
)
|
||||
|
||||
def __init__(self,**kwds):
|
||||
PlotArea.__init__(self)
|
||||
setattr(self,'x',kwds.pop('x',0))
|
||||
setattr(self,'y',kwds.pop('y',0))
|
||||
setattr(self,'width',kwds.pop('width',100))
|
||||
setattr(self,'height',kwds.pop('height',100))
|
||||
setattr(self,'data',kwds.pop('data',[1,1]))
|
||||
setattr(self,'labels',kwds.pop('labels',None))
|
||||
setattr(self,'startAngle',kwds.pop('startAngle',90))
|
||||
setattr(self,'direction',kwds.pop('direction',"clockwise"))
|
||||
setattr(self,'simpleLabels',kwds.pop('simpleLabels',1))
|
||||
setattr(self,'checkLabelOverlap',kwds.pop('checkLabelOverlap',0))
|
||||
setattr(self,'sideLabels',kwds.pop('sideLabels',0))
|
||||
setattr(self,'innerRadiusFraction',kwds.pop('innerRadiusFraction',None))
|
||||
setattr(self,'slices',kwds.pop('slices',TypedPropertyCollection(SectorProperties)))
|
||||
setattr(self,'angleRange',kwds.pop('angleRange',360))
|
||||
|
||||
self.slices[0].fillColor = colors.darkcyan
|
||||
self.slices[1].fillColor = colors.blueviolet
|
||||
self.slices[2].fillColor = colors.blue
|
||||
self.slices[3].fillColor = colors.cyan
|
||||
self.slices[4].fillColor = colors.pink
|
||||
self.slices[5].fillColor = colors.magenta
|
||||
self.slices[6].fillColor = colors.yellow
|
||||
|
||||
|
||||
def demo(self):
|
||||
d = Drawing(200, 100)
|
||||
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 10
|
||||
dn.width = 100
|
||||
dn.height = 80
|
||||
dn.data = [10,20,30,40,50,60]
|
||||
dn.labels = ['a','b','c','d','e','f']
|
||||
|
||||
dn.slices.strokeWidth=0.5
|
||||
dn.slices[3].popout = 10
|
||||
dn.slices[3].strokeWidth = 2
|
||||
dn.slices[3].strokeDashArray = [2,2]
|
||||
dn.slices[3].labelRadius = 1.75
|
||||
dn.slices[3].fontColor = colors.red
|
||||
dn.slices[0].fillColor = colors.darkcyan
|
||||
dn.slices[1].fillColor = colors.blueviolet
|
||||
dn.slices[2].fillColor = colors.blue
|
||||
dn.slices[3].fillColor = colors.cyan
|
||||
dn.slices[4].fillColor = colors.aquamarine
|
||||
dn.slices[5].fillColor = colors.cadetblue
|
||||
dn.slices[6].fillColor = colors.lightcoral
|
||||
|
||||
d.add(dn)
|
||||
return d
|
||||
|
||||
def normalizeData(self, data=None):
|
||||
s = sum(data)
|
||||
f = min(360,self.angleRange)/s if s!=0 else 1
|
||||
return [f*d for d in data]
|
||||
|
||||
def makeSectors(self):
|
||||
# normalize slice data
|
||||
data = self.data
|
||||
multi = isListOfListOfNoneOrNumber(data)
|
||||
if multi:
|
||||
#it's a nested list, more than one sequence
|
||||
normData = []
|
||||
n = []
|
||||
for l in data:
|
||||
t = self.normalizeData(l)
|
||||
normData.append(t)
|
||||
n.append(len(t))
|
||||
self._seriesCount = max(n)
|
||||
else:
|
||||
normData = self.normalizeData(data)
|
||||
n = len(normData)
|
||||
self._seriesCount = n
|
||||
|
||||
#labels
|
||||
checkLabelOverlap = self.checkLabelOverlap
|
||||
L = []
|
||||
L_add = L.append
|
||||
|
||||
labels = self.labels
|
||||
if labels is None:
|
||||
labels = []
|
||||
if not multi:
|
||||
labels = [''] * n
|
||||
else:
|
||||
for m in n:
|
||||
labels = list(labels) + [''] * m
|
||||
else:
|
||||
#there's no point in raising errors for less than enough labels if
|
||||
#we silently create all for the extreme case of no labels.
|
||||
if not multi:
|
||||
i = n-len(labels)
|
||||
if i>0:
|
||||
labels = list(labels) + [''] * i
|
||||
else:
|
||||
tlab = 0
|
||||
for m in n:
|
||||
tlab += m
|
||||
i = tlab-len(labels)
|
||||
if i>0:
|
||||
labels = list(labels) + [''] * i
|
||||
self.labels = labels
|
||||
|
||||
xradius = self.width/2.0
|
||||
yradius = self.height/2.0
|
||||
centerx = self.x + xradius
|
||||
centery = self.y + yradius
|
||||
|
||||
if self.direction == "anticlockwise":
|
||||
whichWay = 1
|
||||
else:
|
||||
whichWay = -1
|
||||
|
||||
g = Group()
|
||||
|
||||
startAngle = self.startAngle #% 360
|
||||
styleCount = len(self.slices)
|
||||
irf = self.innerRadiusFraction
|
||||
|
||||
if multi:
|
||||
#multi-series doughnut
|
||||
ndata = len(data)
|
||||
if irf is None:
|
||||
yir = (yradius/2.5)/ndata
|
||||
xir = (xradius/2.5)/ndata
|
||||
else:
|
||||
yir = yradius*irf
|
||||
xir = xradius*irf
|
||||
ydr = (yradius-yir)/ndata
|
||||
xdr = (xradius-xir)/ndata
|
||||
for sn,series in enumerate(normData):
|
||||
for i,angle in enumerate(series):
|
||||
endAngle = (startAngle + (angle * whichWay)) #% 360
|
||||
aa = abs(startAngle-endAngle)
|
||||
if aa<1e-5:
|
||||
startAngle = endAngle
|
||||
continue
|
||||
if startAngle < endAngle:
|
||||
a1 = startAngle
|
||||
a2 = endAngle
|
||||
else:
|
||||
a1 = endAngle
|
||||
a2 = startAngle
|
||||
startAngle = endAngle
|
||||
|
||||
#if we didn't use %stylecount here we'd end up with the later sectors
|
||||
#all having the default style
|
||||
sectorStyle = self.slices[sn,i%styleCount]
|
||||
|
||||
# is it a popout?
|
||||
cx, cy = centerx, centery
|
||||
if sectorStyle.popout != 0:
|
||||
# pop out the sector
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle * pi/180.0
|
||||
popdistance = sectorStyle.popout
|
||||
cx = centerx + popdistance * cos(aveAngleRadians)
|
||||
cy = centery + popdistance * sin(aveAngleRadians)
|
||||
|
||||
yr1 = yir+sn*ydr
|
||||
yr = yr1 + ydr
|
||||
xr1 = xir+sn*xdr
|
||||
xr = xr1 + xdr
|
||||
if len(series) > 1:
|
||||
theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1)
|
||||
else:
|
||||
theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True)
|
||||
|
||||
theSector.fillColor = sectorStyle.fillColor
|
||||
theSector.strokeColor = sectorStyle.strokeColor
|
||||
theSector.strokeWidth = sectorStyle.strokeWidth
|
||||
theSector.strokeDashArray = sectorStyle.strokeDashArray
|
||||
|
||||
shader = sectorStyle.shadingKind
|
||||
if shader:
|
||||
nshades = aa / float(sectorStyle.shadingAngle)
|
||||
if nshades > 1:
|
||||
shader = colors.Whiter if shader=='lighten' else colors.Blacker
|
||||
nshades = 1+int(nshades)
|
||||
shadingAmount = 1-sectorStyle.shadingAmount
|
||||
if sectorStyle.shadingDirection=='normal':
|
||||
dsh = (1-shadingAmount)/float(nshades-1)
|
||||
shf1 = shadingAmount
|
||||
else:
|
||||
dsh = (shadingAmount-1)/float(nshades-1)
|
||||
shf1 = 1
|
||||
shda = (a2-a1)/float(nshades)
|
||||
shsc = sectorStyle.fillColor
|
||||
theSector.fillColor = None
|
||||
for ish in range(nshades):
|
||||
sha1 = a1 + ish*shda
|
||||
sha2 = a1 + (ish+1)*shda
|
||||
shc = shader(shsc,shf1 + dsh*ish)
|
||||
if len(series)>1:
|
||||
shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1)
|
||||
else:
|
||||
shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True)
|
||||
shSector.fillColor = shc
|
||||
shSector.strokeColor = None
|
||||
shSector.strokeWidth = 0
|
||||
g.add(shSector)
|
||||
|
||||
g.add(theSector)
|
||||
|
||||
if sn == 0 and sectorStyle.visible and sectorStyle.label_visible:
|
||||
text = self.getSeriesName(i,'')
|
||||
if text:
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle*pi/180.0
|
||||
labelRadius = sectorStyle.labelRadius
|
||||
rx = xradius*labelRadius
|
||||
ry = yradius*labelRadius
|
||||
labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius)
|
||||
labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius)
|
||||
l = _addWedgeLabel(self,text,averageAngle,labelX,labelY,sectorStyle)
|
||||
if checkLabelOverlap:
|
||||
l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle,
|
||||
'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy,
|
||||
'bounds': l.getBounds(),
|
||||
}
|
||||
L_add(l)
|
||||
|
||||
else:
|
||||
#single series doughnut
|
||||
if irf is None:
|
||||
yir = yradius/2.5
|
||||
xir = xradius/2.5
|
||||
else:
|
||||
yir = yradius*irf
|
||||
xir = xradius*irf
|
||||
for i,angle in enumerate(normData):
|
||||
endAngle = (startAngle + (angle * whichWay)) #% 360
|
||||
aa = abs(startAngle-endAngle)
|
||||
if aa<1e-5:
|
||||
startAngle = endAngle
|
||||
continue
|
||||
if startAngle < endAngle:
|
||||
a1 = startAngle
|
||||
a2 = endAngle
|
||||
else:
|
||||
a1 = endAngle
|
||||
a2 = startAngle
|
||||
startAngle = endAngle
|
||||
|
||||
#if we didn't use %stylecount here we'd end up with the later sectors
|
||||
#all having the default style
|
||||
sectorStyle = self.slices[i%styleCount]
|
||||
|
||||
# is it a popout?
|
||||
cx, cy = centerx, centery
|
||||
if sectorStyle.popout != 0:
|
||||
# pop out the sector
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle * pi/180.0
|
||||
popdistance = sectorStyle.popout
|
||||
cx = centerx + popdistance * cos(aveAngleRadians)
|
||||
cy = centery + popdistance * sin(aveAngleRadians)
|
||||
|
||||
if n > 1:
|
||||
theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir)
|
||||
elif n==1:
|
||||
theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir, annular=True)
|
||||
|
||||
theSector.fillColor = sectorStyle.fillColor
|
||||
theSector.strokeColor = sectorStyle.strokeColor
|
||||
theSector.strokeWidth = sectorStyle.strokeWidth
|
||||
theSector.strokeDashArray = sectorStyle.strokeDashArray
|
||||
|
||||
shader = sectorStyle.shadingKind
|
||||
if shader:
|
||||
nshades = aa / float(sectorStyle.shadingAngle)
|
||||
if nshades > 1:
|
||||
shader = colors.Whiter if shader=='lighten' else colors.Blacker
|
||||
nshades = 1+int(nshades)
|
||||
shadingAmount = 1-sectorStyle.shadingAmount
|
||||
if sectorStyle.shadingDirection=='normal':
|
||||
dsh = (1-shadingAmount)/float(nshades-1)
|
||||
shf1 = shadingAmount
|
||||
else:
|
||||
dsh = (shadingAmount-1)/float(nshades-1)
|
||||
shf1 = 1
|
||||
shda = (a2-a1)/float(nshades)
|
||||
shsc = sectorStyle.fillColor
|
||||
theSector.fillColor = None
|
||||
for ish in range(nshades):
|
||||
sha1 = a1 + ish*shda
|
||||
sha2 = a1 + (ish+1)*shda
|
||||
shc = shader(shsc,shf1 + dsh*ish)
|
||||
if n > 1:
|
||||
shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir)
|
||||
elif n==1:
|
||||
shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir, annular=True)
|
||||
shSector.fillColor = shc
|
||||
shSector.strokeColor = None
|
||||
shSector.strokeWidth = 0
|
||||
g.add(shSector)
|
||||
|
||||
g.add(theSector)
|
||||
|
||||
# now draw a label
|
||||
if labels[i] and sectorStyle.visible and sectorStyle.label_visible:
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle*pi/180.0
|
||||
labelRadius = sectorStyle.labelRadius
|
||||
labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius)
|
||||
labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius)
|
||||
rx = xradius*labelRadius
|
||||
ry = yradius*labelRadius
|
||||
l = _addWedgeLabel(self,labels[i],averageAngle,labelX,labelY,sectorStyle)
|
||||
if checkLabelOverlap:
|
||||
l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle,
|
||||
'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy,
|
||||
'bounds': l.getBounds(),
|
||||
}
|
||||
L_add(l)
|
||||
|
||||
if checkLabelOverlap and L:
|
||||
fixLabelOverlaps(L)
|
||||
|
||||
for l in L: g.add(l)
|
||||
|
||||
return g
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
g.add(self.makeSectors())
|
||||
return g
|
||||
|
||||
|
||||
def sample1():
|
||||
"Make up something from the individual Sectors"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
g = Group()
|
||||
|
||||
s1 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=0, endangledegrees=120, radius1=100)
|
||||
s1.fillColor=colors.red
|
||||
s1.strokeColor=None
|
||||
d.add(s1)
|
||||
s2 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=120, endangledegrees=240, radius1=100)
|
||||
s2.fillColor=colors.green
|
||||
s2.strokeColor=None
|
||||
d.add(s2)
|
||||
s3 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=240, endangledegrees=260, radius1=100)
|
||||
s3.fillColor=colors.blue
|
||||
s3.strokeColor=None
|
||||
d.add(s3)
|
||||
s4 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=260, endangledegrees=360, radius1=100)
|
||||
s4.fillColor=colors.gray
|
||||
s4.strokeColor=None
|
||||
d.add(s4)
|
||||
|
||||
return d
|
||||
|
||||
def sample2():
|
||||
"Make a simple demo"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 50
|
||||
dn.width = 300
|
||||
dn.height = 300
|
||||
dn.data = [10,20,30,40,50,60]
|
||||
|
||||
d.add(dn)
|
||||
|
||||
return d
|
||||
|
||||
def sample3():
|
||||
"Make a more complex demo"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 50
|
||||
dn.width = 300
|
||||
dn.height = 300
|
||||
dn.data = [[10,20,30,40,50,60], [10,20,30,40]]
|
||||
dn.labels = ['a','b','c','d','e','f']
|
||||
|
||||
d.add(dn)
|
||||
|
||||
return d
|
||||
|
||||
def sample4():
|
||||
"Make a more complex demo with Label Overlap fixing"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 50
|
||||
dn.width = 300
|
||||
dn.height = 300
|
||||
dn.data = [[10,20,30,40,50,60], [10,20,30,40]]
|
||||
dn.labels = ['a','b','c','d','e','f']
|
||||
dn.checkLabelOverlap = True
|
||||
|
||||
d.add(dn)
|
||||
|
||||
return d
|
||||
|
||||
if __name__=='__main__':
|
||||
|
||||
from reportlab.graphics.renderPDF import drawToFile
|
||||
d = sample1()
|
||||
drawToFile(d, 'doughnut1.pdf')
|
||||
d = sample2()
|
||||
drawToFile(d, 'doughnut2.pdf')
|
||||
d = sample3()
|
||||
drawToFile(d, 'doughnut3.pdf')
|
||||
@@ -0,0 +1,647 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/legends.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""This will be a collection of legends to be used with charts."""
|
||||
|
||||
import copy
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\
|
||||
isNumberOrNone, isListOfNumbersOrNone, isBoolean,\
|
||||
EitherOr, NoneOr, AutoOr, isAuto, Auto, isBoxAnchor, SequenceOf, isInstanceOf
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth, getFont
|
||||
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
|
||||
from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS
|
||||
from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol
|
||||
from reportlab.lib.utils import isSeq, find_locals, isStr, asNative
|
||||
from reportlab.graphics.shapes import _baseGFontName
|
||||
|
||||
def _transMax(n,A):
|
||||
X = n*[0]
|
||||
m = 0
|
||||
for a in A:
|
||||
m = max(m,len(a))
|
||||
for i,x in enumerate(a):
|
||||
X[i] = max(X[i],x)
|
||||
X = [0] + X[:m]
|
||||
for i in range(m):
|
||||
X[i+1] += X[i]
|
||||
return X
|
||||
|
||||
def _objStr(s):
|
||||
if isStr(s):
|
||||
return asNative(s)
|
||||
else:
|
||||
return str(s)
|
||||
|
||||
def _getStr(s):
|
||||
if isSeq(s):
|
||||
return list(map(_getStr,s))
|
||||
else:
|
||||
return _objStr(s)
|
||||
|
||||
def _getLines(s):
|
||||
if isSeq(s):
|
||||
return tuple([(x or '').split('\n') for x in s])
|
||||
else:
|
||||
return (s or '').split('\n')
|
||||
|
||||
def _getLineCount(s):
|
||||
T = _getLines(s)
|
||||
if isSeq(s):
|
||||
return max([len(x) for x in T])
|
||||
else:
|
||||
return len(T)
|
||||
|
||||
def _getWidths(i,s, fontName, fontSize, subCols):
|
||||
S = []
|
||||
aS = S.append
|
||||
if isSeq(s):
|
||||
for j,t in enumerate(s):
|
||||
sc = subCols[j,i]
|
||||
fN = getattr(sc,'fontName',fontName)
|
||||
fS = getattr(sc,'fontSize',fontSize)
|
||||
m = [stringWidth(x, fN, fS) for x in t.split('\n')]
|
||||
m = max(sc.minWidth,m and max(m) or 0)
|
||||
aS(m)
|
||||
aS(sc.rpad)
|
||||
del S[-1]
|
||||
else:
|
||||
sc = subCols[0,i]
|
||||
fN = getattr(sc,'fontName',fontName)
|
||||
fS = getattr(sc,'fontSize',fontSize)
|
||||
m = [stringWidth(x, fN, fS) for x in s.split('\n')]
|
||||
aS(max(sc.minWidth,m and max(m) or 0))
|
||||
return S
|
||||
|
||||
class SubColProperty(PropHolder):
|
||||
dividerLines = 0
|
||||
_attrMap = AttrMap(
|
||||
minWidth = AttrMapValue(isNumber,desc="minimum width for this subcol"),
|
||||
rpad = AttrMapValue(isNumber,desc="right padding for this subcol"),
|
||||
align = AttrMapValue(OneOf('left','right','center','centre','numeric'),desc='alignment in subCol'),
|
||||
fontName = AttrMapValue(isString, desc="Font name of the strings"),
|
||||
fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
|
||||
leading = AttrMapValue(isNumberOrNone, desc="leading for the strings"),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc="fontColor"),
|
||||
underlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="underline definitions"),
|
||||
overlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="overline definitions"),
|
||||
dx = AttrMapValue(isNumber, desc="x offset from default position"),
|
||||
dy = AttrMapValue(isNumber, desc="y offset from default position"),
|
||||
vAlign = AttrMapValue(OneOf('top','bottom','middle'),desc='vertical alignment in the row'),
|
||||
)
|
||||
|
||||
class LegendCallout:
|
||||
def _legendValues(legend,*args):
|
||||
'''return a tuple of values from the first function up the stack with isinstance(self,legend)'''
|
||||
L = find_locals(lambda L: L.get('self',None) is legend and L or None)
|
||||
return tuple([L[a] for a in args])
|
||||
_legendValues = staticmethod(_legendValues)
|
||||
|
||||
def _selfOrLegendValues(self,legend,*args):
|
||||
L = find_locals(lambda L: L.get('self',None) is legend and L or None)
|
||||
return tuple([getattr(self,a,L[a]) for a in args])
|
||||
|
||||
def __call__(self,legend,g,thisx,y,colName):
|
||||
col, name = colName
|
||||
|
||||
class LegendSwatchCallout(LegendCallout):
|
||||
def __call__(self,legend,g,thisx,y,i,colName,swatch):
|
||||
col, name = colName
|
||||
|
||||
class LegendColEndCallout(LegendCallout):
|
||||
def __call__(self,legend, g, x, xt, y, width, lWidth):
|
||||
pass
|
||||
|
||||
class Legend(Widget):
|
||||
"""A simple legend containing rectangular swatches and strings.
|
||||
|
||||
The swatches are filled rectangles whenever the respective
|
||||
color object in 'colorNamePairs' is a subclass of Color in
|
||||
reportlab.lib.colors. Otherwise the object passed instead is
|
||||
assumed to have 'x', 'y', 'width' and 'height' attributes.
|
||||
A legend then tries to set them or catches any error. This
|
||||
lets you plug-in any widget you like as a replacement for
|
||||
the default rectangular swatches.
|
||||
|
||||
Strings can be nicely aligned left or right to the swatches.
|
||||
"""
|
||||
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc="x-coordinate of upper-left reference point"),
|
||||
y = AttrMapValue(isNumber, desc="y-coordinate of upper-left reference point"),
|
||||
deltax = AttrMapValue(isNumberOrNone, desc="x-distance between neighbouring swatches"),
|
||||
deltay = AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring swatches"),
|
||||
dxTextSpace = AttrMapValue(isNumber, desc="Distance between swatch rectangle and text"),
|
||||
autoXPadding = AttrMapValue(isNumber, desc="x Padding between columns if deltax=None",advancedUsage=1),
|
||||
autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None",advancedUsage=1),
|
||||
yGap = AttrMapValue(isNumber, desc="Additional gap between rows",advancedUsage=1),
|
||||
dx = AttrMapValue(isNumber, desc="Width of swatch rectangle"),
|
||||
dy = AttrMapValue(isNumber, desc="Height of swatch rectangle"),
|
||||
columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"),
|
||||
alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to swatches"),
|
||||
colorNamePairs = AttrMapValue(None, desc="List of color/name tuples (color can also be widget)"),
|
||||
fontName = AttrMapValue(isString, desc="Font name of the strings"),
|
||||
fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
|
||||
leading = AttrMapValue(isNumberOrNone, desc="text leading"),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc="swatches filling color"),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc="Border color of the swatches"),
|
||||
strokeWidth = AttrMapValue(isNumber, desc="Width of the border color of the swatches"),
|
||||
swatchMarker = AttrMapValue(NoneOr(AutoOr(isSymbol)), desc="None, Auto() or makeMarker('Diamond') ...",advancedUsage=1),
|
||||
callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))",advancedUsage=1),
|
||||
boxAnchor = AttrMapValue(isBoxAnchor,'Anchor point for the legend area'),
|
||||
variColumn = AttrMapValue(isBoolean,'If true column widths may vary (default is false)',advancedUsage=1),
|
||||
dividerLines = AttrMapValue(OneOf(0,1,2,3,4,5,6,7),'If 1 we have dividers between the rows | 2 for extra top | 4 for bottom',advancedUsage=1),
|
||||
dividerWidth = AttrMapValue(isNumber, desc="dividerLines width",advancedUsage=1),
|
||||
dividerColor = AttrMapValue(isColorOrNone, desc="dividerLines color",advancedUsage=1),
|
||||
dividerDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.',advancedUsage=1),
|
||||
dividerOffsX = AttrMapValue(SequenceOf(isNumber,emptyOK=0,lo=2,hi=2), desc='divider lines X offsets',advancedUsage=1),
|
||||
dividerOffsY = AttrMapValue(isNumber, desc="dividerLines Y offset",advancedUsage=1),
|
||||
colEndCallout = AttrMapValue(None, desc="a user callout(self,g, x, xt, y,width, lWidth)",advancedUsage=1),
|
||||
subCols = AttrMapValue(None,desc="subColumn properties"),
|
||||
swatchCallout = AttrMapValue(None, desc="a user swatch callout(self,g,x,y,i,(col,name),swatch)",advancedUsage=1),
|
||||
swdx = AttrMapValue(isNumber, desc="x position adjustment for the swatch"),
|
||||
swdy = AttrMapValue(isNumber, desc="y position adjustment for the swatch"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
# Upper-left reference point.
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
|
||||
# Alginment of text with respect to swatches.
|
||||
self.alignment = "left"
|
||||
|
||||
# x- and y-distances between neighbouring swatches.
|
||||
self.deltax = 75
|
||||
self.deltay = 20
|
||||
self.autoXPadding = 5
|
||||
self.autoYPadding = 2
|
||||
|
||||
# Size of swatch rectangle.
|
||||
self.dx = 10
|
||||
self.dy = 10
|
||||
|
||||
self.swdx = 0
|
||||
self.swdy = 0
|
||||
|
||||
# Distance between swatch rectangle and text.
|
||||
self.dxTextSpace = 10
|
||||
|
||||
# Max. number of items per column.
|
||||
self.columnMaximum = 3
|
||||
|
||||
# Color/name pairs.
|
||||
self.colorNamePairs = [ (colors.red, "red"),
|
||||
(colors.blue, "blue"),
|
||||
(colors.green, "green"),
|
||||
(colors.pink, "pink"),
|
||||
(colors.yellow, "yellow") ]
|
||||
|
||||
# Font name and size of the labels.
|
||||
self.fontName = STATE_DEFAULTS['fontName']
|
||||
self.fontSize = STATE_DEFAULTS['fontSize']
|
||||
self.leading = None #will be used as 1.2*fontSize
|
||||
self.fillColor = STATE_DEFAULTS['fillColor']
|
||||
self.strokeColor = STATE_DEFAULTS['strokeColor']
|
||||
self.strokeWidth = STATE_DEFAULTS['strokeWidth']
|
||||
self.swatchMarker = None
|
||||
self.boxAnchor = 'nw'
|
||||
self.yGap = 0
|
||||
self.variColumn = 0
|
||||
self.dividerLines = 0
|
||||
self.dividerWidth = 0.5
|
||||
self.dividerDashArray = None
|
||||
self.dividerColor = colors.black
|
||||
self.dividerOffsX = (0,0)
|
||||
self.dividerOffsY = 0
|
||||
self.colEndCallout = None
|
||||
self._init_subCols()
|
||||
|
||||
def _init_subCols(self):
|
||||
sc = self.subCols = TypedPropertyCollection(SubColProperty)
|
||||
sc.rpad = 1
|
||||
sc.dx = sc.dy = sc.minWidth = 0
|
||||
sc.align = 'right'
|
||||
sc[0].align = 'left'
|
||||
sc.vAlign = 'top' #that's current
|
||||
sc.leading = None
|
||||
|
||||
def _getChartStyleName(self,chart):
|
||||
for a in 'lines', 'bars', 'slices', 'strands':
|
||||
if hasattr(chart,a): return a
|
||||
return None
|
||||
|
||||
def _getChartStyle(self,chart):
|
||||
return getattr(chart,self._getChartStyleName(chart),None)
|
||||
|
||||
def _getTexts(self,colorNamePairs):
|
||||
if not isAuto(colorNamePairs):
|
||||
texts = [_getStr(p[1]) for p in colorNamePairs]
|
||||
else:
|
||||
chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
|
||||
texts = [chart.getSeriesName(i,'series %d' % i) for i in range(chart._seriesCount)]
|
||||
return texts
|
||||
|
||||
def _calculateMaxBoundaries(self, colorNamePairs):
|
||||
"Calculate the maximum width of some given strings."
|
||||
fontName = self.fontName
|
||||
fontSize = self.fontSize
|
||||
subCols = self.subCols
|
||||
|
||||
M = [_getWidths(i, m, fontName, fontSize, subCols) for i,m in enumerate(self._getTexts(colorNamePairs))]
|
||||
if not M:
|
||||
return [0,0]
|
||||
n = max([len(m) for m in M])
|
||||
if self.variColumn:
|
||||
columnMaximum = self.columnMaximum
|
||||
return [_transMax(n,M[r:r+columnMaximum]) for r in range(0,len(M),self.columnMaximum)]
|
||||
else:
|
||||
return _transMax(n,M)
|
||||
|
||||
def _calcHeight(self):
|
||||
dy = self.dy
|
||||
yGap = self.yGap
|
||||
thisy = upperlefty = self.y - dy
|
||||
fontSize = self.fontSize
|
||||
fontName = self.fontName
|
||||
ascent=getFont(fontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent *= fontSize
|
||||
leading = fontSize*1.2
|
||||
deltay = self.deltay
|
||||
if not deltay: deltay = max(dy,leading)+self.autoYPadding
|
||||
columnCount = 0
|
||||
count = 0
|
||||
lowy = upperlefty
|
||||
lim = self.columnMaximum - 1
|
||||
for name in self._getTexts(self.colorNamePairs):
|
||||
y0 = thisy+(dy-ascent)*0.5
|
||||
y = y0 - _getLineCount(name)*leading
|
||||
leadingMove = 2*y0-y-thisy
|
||||
newy = thisy-max(deltay,leadingMove)-yGap
|
||||
lowy = min(y,newy,lowy)
|
||||
if count==lim:
|
||||
count = 0
|
||||
thisy = upperlefty
|
||||
columnCount += 1
|
||||
else:
|
||||
thisy = newy
|
||||
count = count+1
|
||||
return upperlefty - lowy
|
||||
|
||||
def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
|
||||
return Rect(x, thisy, dx, dy,
|
||||
fillColor = fillColor,
|
||||
strokeColor = strokeColor,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
|
||||
def draw(self):
|
||||
colorNamePairs = self.colorNamePairs
|
||||
autoCP = isAuto(colorNamePairs)
|
||||
if autoCP:
|
||||
chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
|
||||
swatchMarker = None
|
||||
autoCP = Auto(obj=chart)
|
||||
n = chart._seriesCount
|
||||
chartTexts = self._getTexts(colorNamePairs)
|
||||
else:
|
||||
swatchMarker = getattr(self,'swatchMarker',None)
|
||||
if isAuto(swatchMarker):
|
||||
chart = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None))
|
||||
swatchMarker = Auto(obj=chart)
|
||||
n = len(colorNamePairs)
|
||||
dx = self.dx
|
||||
dy = self.dy
|
||||
alignment = self.alignment
|
||||
columnMaximum = self.columnMaximum
|
||||
deltax = self.deltax
|
||||
deltay = self.deltay
|
||||
dxTextSpace = self.dxTextSpace
|
||||
fontName = self.fontName
|
||||
fontSize = self.fontSize
|
||||
fillColor = self.fillColor
|
||||
strokeWidth = self.strokeWidth
|
||||
strokeColor = self.strokeColor
|
||||
subCols = self.subCols
|
||||
leading = fontSize*1.2
|
||||
yGap = self.yGap
|
||||
if not deltay:
|
||||
deltay = max(dy,leading)+self.autoYPadding
|
||||
ba = self.boxAnchor
|
||||
maxWidth = self._calculateMaxBoundaries(colorNamePairs)
|
||||
nCols = int((n+columnMaximum-1)/(columnMaximum*1.0))
|
||||
xW = dx+dxTextSpace+self.autoXPadding
|
||||
variColumn = self.variColumn
|
||||
if variColumn:
|
||||
width = sum([m[-1] for m in maxWidth])+xW*nCols
|
||||
else:
|
||||
deltax = max(maxWidth[-1]+xW,deltax)
|
||||
width = nCols*deltax
|
||||
maxWidth = nCols*[maxWidth]
|
||||
|
||||
thisx = self.x
|
||||
thisy = self.y - self.dy
|
||||
if ba not in ('ne','n','nw','autoy'):
|
||||
height = self._calcHeight()
|
||||
if ba in ('e','c','w'):
|
||||
thisy += height/2.
|
||||
else:
|
||||
thisy += height
|
||||
if ba not in ('nw','w','sw','autox'):
|
||||
if ba in ('n','c','s'):
|
||||
thisx -= width/2
|
||||
else:
|
||||
thisx -= width
|
||||
upperlefty = thisy
|
||||
|
||||
g = Group()
|
||||
|
||||
ascent=getFont(fontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent *= fontSize # normalize
|
||||
|
||||
lim = columnMaximum - 1
|
||||
callout = getattr(self,'callout',None)
|
||||
scallout = getattr(self,'swatchCallout',None)
|
||||
dividerLines = self.dividerLines
|
||||
if dividerLines:
|
||||
dividerWidth = self.dividerWidth
|
||||
dividerColor = self.dividerColor
|
||||
dividerDashArray = self.dividerDashArray
|
||||
dividerOffsX = self.dividerOffsX
|
||||
dividerOffsY = self.dividerOffsY
|
||||
|
||||
for i in range(n):
|
||||
if autoCP:
|
||||
col = autoCP
|
||||
col.index = i
|
||||
name = chartTexts[i]
|
||||
else:
|
||||
col, name = colorNamePairs[i]
|
||||
if isAuto(swatchMarker):
|
||||
col = swatchMarker
|
||||
col.index = i
|
||||
if isAuto(name):
|
||||
name = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)).getSeriesName(i,'series %d' % i)
|
||||
T = _getLines(name)
|
||||
S = []
|
||||
aS = S.append
|
||||
j = int(i/(columnMaximum*1.0))
|
||||
jOffs = maxWidth[j]
|
||||
|
||||
# thisy+dy/2 = y+leading/2
|
||||
y = y0 = thisy+(dy-ascent)*0.5
|
||||
|
||||
if callout: callout(self,g,thisx,y,(col,name))
|
||||
if alignment == "left":
|
||||
x = thisx
|
||||
xn = thisx+jOffs[-1]+dxTextSpace
|
||||
elif alignment == "right":
|
||||
x = thisx+dx+dxTextSpace
|
||||
xn = thisx
|
||||
else:
|
||||
raise ValueError("bad alignment")
|
||||
if not isSeq(name):
|
||||
T = [T]
|
||||
lineCount = _getLineCount(name)
|
||||
yd = y
|
||||
for k,lines in enumerate(T):
|
||||
y = y0
|
||||
kk = k*2
|
||||
x1 = x+jOffs[kk]
|
||||
x2 = x+jOffs[kk+1]
|
||||
sc = subCols[k,i]
|
||||
anchor = sc.align
|
||||
scdx = sc.dx
|
||||
scdy = sc.dy
|
||||
fN = getattr(sc,'fontName',fontName)
|
||||
fS = getattr(sc,'fontSize',fontSize)
|
||||
fC = getattr(sc,'fillColor',fillColor)
|
||||
fL = sc.leading or 1.2*fontSize
|
||||
if fN==fontName:
|
||||
fA = (ascent*fS)/fontSize
|
||||
else:
|
||||
fA = getFont(fontName).face.ascent/1000.
|
||||
if fA==0: fA=0.718
|
||||
fA *= fS
|
||||
|
||||
vA = sc.vAlign
|
||||
if vA=='top':
|
||||
vAdy = 0
|
||||
else:
|
||||
vAdy = -fL * (lineCount - len(lines))
|
||||
if vA=='middle': vAdy *= 0.5
|
||||
|
||||
if anchor=='left':
|
||||
anchor = 'start'
|
||||
xoffs = x1
|
||||
elif anchor=='right':
|
||||
anchor = 'end'
|
||||
xoffs = x2
|
||||
elif anchor=='numeric':
|
||||
xoffs = x2
|
||||
else:
|
||||
anchor = 'middle'
|
||||
xoffs = 0.5*(x1+x2)
|
||||
for t in lines:
|
||||
aS(String(xoffs+scdx,y+scdy+vAdy,t,fontName=fN,fontSize=fS,fillColor=fC, textAnchor = anchor))
|
||||
y -= fL
|
||||
yd = min(yd,y)
|
||||
y += fL
|
||||
for iy, a in ((y-max(fL-fA,0),'underlines'),(y+fA,'overlines')):
|
||||
il = getattr(sc,a,None)
|
||||
if il:
|
||||
if not isinstance(il,(tuple,list)): il = (il,)
|
||||
for l in il:
|
||||
l = copy.copy(l)
|
||||
l.y1 += iy
|
||||
l.y2 += iy
|
||||
l.x1 += x1
|
||||
l.x2 += x2
|
||||
aS(l)
|
||||
x = xn
|
||||
y = yd
|
||||
leadingMove = 2*y0-y-thisy
|
||||
|
||||
if dividerLines:
|
||||
xd = thisx+dx+dxTextSpace+jOffs[-1]+dividerOffsX[1]
|
||||
yd = thisy+dy*0.5+dividerOffsY
|
||||
if ((dividerLines&1) and i%columnMaximum) or ((dividerLines&2) and not i%columnMaximum):
|
||||
g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
|
||||
strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
|
||||
|
||||
if (dividerLines&4) and (i%columnMaximum==lim or i==(n-1)):
|
||||
yd -= max(deltay,leadingMove)+yGap
|
||||
g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
|
||||
strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
|
||||
|
||||
# Make a 'normal' color swatch...
|
||||
swatchX = x + getattr(self,'swdx',0)
|
||||
swatchY = thisy + getattr(self,'swdy',0)
|
||||
|
||||
if isAuto(col):
|
||||
chart = getattr(col,'chart',getattr(col,'obj',None))
|
||||
c = chart.makeSwatchSample(getattr(col,'index',i),swatchX,swatchY,dx,dy)
|
||||
elif isinstance(col, colors.Color):
|
||||
if isSymbol(swatchMarker):
|
||||
c = uSymbol2Symbol(swatchMarker,swatchX+dx/2.,swatchY+dy/2.,col)
|
||||
else:
|
||||
c = self._defaultSwatch(swatchX,swatchY,dx,dy,fillColor=col,strokeWidth=strokeWidth,strokeColor=strokeColor)
|
||||
elif col is not None:
|
||||
try:
|
||||
c = copy.deepcopy(col)
|
||||
c.x = swatchX
|
||||
c.y = swatchY
|
||||
c.width = dx
|
||||
c.height = dy
|
||||
except:
|
||||
c = None
|
||||
else:
|
||||
c = None
|
||||
|
||||
if c:
|
||||
g.add(c)
|
||||
if scallout: scallout(self,g,thisx,y0,i,(col,name),c)
|
||||
|
||||
for s in S: g.add(s)
|
||||
if self.colEndCallout and (i%columnMaximum==lim or i==(n-1)):
|
||||
if alignment == "left":
|
||||
xt = thisx
|
||||
else:
|
||||
xt = thisx+dx+dxTextSpace
|
||||
yd = thisy+dy*0.5+dividerOffsY - (max(deltay,leadingMove)+yGap)
|
||||
self.colEndCallout(self, g, thisx, xt, yd, jOffs[-1], jOffs[-1]+dx+dxTextSpace)
|
||||
|
||||
if i%columnMaximum==lim:
|
||||
if variColumn:
|
||||
thisx += jOffs[-1]+xW
|
||||
else:
|
||||
thisx = thisx+deltax
|
||||
thisy = upperlefty
|
||||
else:
|
||||
thisy = thisy-max(deltay,leadingMove)-yGap
|
||||
|
||||
return g
|
||||
|
||||
def demo(self):
|
||||
"Make sample legend."
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
legend = Legend()
|
||||
legend.alignment = 'left'
|
||||
legend.x = 0
|
||||
legend.y = 100
|
||||
legend.dxTextSpace = 5
|
||||
items = 'red green blue yellow pink black white'.split()
|
||||
items = [(getattr(colors, i), i) for i in items]
|
||||
legend.colorNamePairs = items
|
||||
|
||||
d.add(legend, 'legend')
|
||||
|
||||
return d
|
||||
|
||||
class TotalAnnotator(LegendColEndCallout):
|
||||
def __init__(self, lText='Total', rText='0.0', fontName=_baseGFontName, fontSize=10,
|
||||
fillColor=colors.black, strokeWidth=0.5, strokeColor=colors.black, strokeDashArray=None,
|
||||
dx=0, dy=0, dly=0, dlx=(0,0)):
|
||||
self.lText = lText
|
||||
self.rText = rText
|
||||
self.fontName = fontName
|
||||
self.fontSize = fontSize
|
||||
self.fillColor = fillColor
|
||||
self.dy = dy
|
||||
self.dx = dx
|
||||
self.dly = dly
|
||||
self.dlx = dlx
|
||||
self.strokeWidth = strokeWidth
|
||||
self.strokeColor = strokeColor
|
||||
self.strokeDashArray = strokeDashArray
|
||||
|
||||
def __call__(self,legend, g, x, xt, y, width, lWidth):
|
||||
from reportlab.graphics.shapes import String, Line
|
||||
fontSize = self.fontSize
|
||||
fontName = self.fontName
|
||||
fillColor = self.fillColor
|
||||
strokeColor = self.strokeColor
|
||||
strokeWidth = self.strokeWidth
|
||||
ascent=getFont(fontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent *= fontSize
|
||||
leading = fontSize*1.2
|
||||
yt = y+self.dy-ascent*1.3
|
||||
if self.lText and fillColor:
|
||||
g.add(String(xt,yt,self.lText,
|
||||
fontName=fontName,
|
||||
fontSize=fontSize,
|
||||
fillColor=fillColor,
|
||||
textAnchor = "start"))
|
||||
if self.rText:
|
||||
g.add(String(xt+width,yt,self.rText,
|
||||
fontName=fontName,
|
||||
fontSize=fontSize,
|
||||
fillColor=fillColor,
|
||||
textAnchor = "end"))
|
||||
if strokeWidth and strokeColor:
|
||||
yL = y+self.dly-leading
|
||||
g.add(Line(x+self.dlx[0],yL,x+self.dlx[1]+lWidth,yL,
|
||||
strokeColor=strokeColor, strokeWidth=strokeWidth,
|
||||
strokeDashArray=self.strokeDashArray))
|
||||
|
||||
class LineSwatch(Widget):
|
||||
"""basically a Line with properties added so it can be used in a LineLegend"""
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc="x-coordinate for swatch line start point"),
|
||||
y = AttrMapValue(isNumber, desc="y-coordinate for swatch line start point"),
|
||||
width = AttrMapValue(isNumber, desc="length of swatch line"),
|
||||
height = AttrMapValue(isNumber, desc="used for line strokeWidth"),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc="color of swatch line"),
|
||||
strokeWidth = AttrMapValue(isNumberOrNone, desc="thickness of the swatch"),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
from reportlab.lib.colors import red
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.width = 20
|
||||
self.height = 1
|
||||
self.strokeColor = red
|
||||
self.strokeDashArray = None
|
||||
self.strokeWidth = 1
|
||||
|
||||
def draw(self):
|
||||
l = Line(self.x,self.y,self.x+self.width,self.y)
|
||||
l.strokeColor = self.strokeColor
|
||||
l.strokeDashArray = self.strokeDashArray
|
||||
l.strokeWidth = self.strokeWidth
|
||||
return l
|
||||
|
||||
class LineLegend(Legend):
|
||||
"""A subclass of Legend for drawing legends with lines as the
|
||||
swatches rather than rectangles. Useful for lineCharts and
|
||||
linePlots. Should be similar in all other ways the the standard
|
||||
Legend class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Legend.__init__(self)
|
||||
|
||||
# Size of swatch rectangle.
|
||||
self.dx = 10
|
||||
self.dy = 2
|
||||
|
||||
def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
|
||||
l = LineSwatch()
|
||||
l.x = x
|
||||
l.y = thisy
|
||||
l.width = dx
|
||||
l.height = dy
|
||||
l.strokeColor = fillColor
|
||||
l.strokeWidth = strokeWidth
|
||||
return l
|
||||
@@ -0,0 +1,801 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/linecharts.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""This modules defines a very preliminary Line Chart example."""
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isNumberOrNone, isColorOrNone, \
|
||||
isListOfStringsOrNone, isBoolean, NoneOr, \
|
||||
isListOfNumbersOrNone, isStringOrNone, OneOf, Percentage
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.lib.utils import flatten
|
||||
from reportlab.graphics.widgetbase import TypedPropertyCollection, PropHolder, tpcGetItem
|
||||
from reportlab.graphics.shapes import Line, Rect, Group, Drawing, Polygon, PolyLine
|
||||
from reportlab.graphics.widgets.signsandsymbols import NoEntry
|
||||
from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis, YCategoryAxis, XValueAxis
|
||||
from reportlab.graphics.charts.textlabels import Label
|
||||
from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
from reportlab.graphics.charts.legends import _objStr
|
||||
from .utils import FillPairedData
|
||||
|
||||
class LineChartProperties(PropHolder):
|
||||
_attrMap = AttrMap(
|
||||
strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line or border.'),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc='fill color of a bar.'),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'),
|
||||
symbol = AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.',advancedUsage=1),
|
||||
shader = AttrMapValue(None, desc='Shader Class.',advancedUsage=1),
|
||||
filler = AttrMapValue(None, desc='Filler Class.',advancedUsage=1),
|
||||
name = AttrMapValue(isStringOrNone, desc='Name of the line.'),
|
||||
lineStyle = AttrMapValue(NoneOr(OneOf('line','joinedLine','bar')), desc="What kind of plot this line is",advancedUsage=1),
|
||||
barWidth = AttrMapValue(isNumberOrNone,desc="Percentage of available width to be used for a bar",advancedUsage=1),
|
||||
inFill = AttrMapValue(isBoolean, desc='If true flood fill to x axis',advancedUsage=1),
|
||||
)
|
||||
|
||||
class AbstractLineChart(PlotArea):
|
||||
|
||||
def makeSwatchSample(self,rowNo, x, y, width, height):
|
||||
baseStyle = self.lines
|
||||
styleIdx = rowNo % len(baseStyle)
|
||||
style = baseStyle[styleIdx]
|
||||
color = style.strokeColor
|
||||
yh2 = y+height/2.
|
||||
lineStyle = getattr(style,'lineStyle',None)
|
||||
if lineStyle=='bar':
|
||||
dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
|
||||
strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None))
|
||||
L = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=color,strokeLineCap=0,strokeDashArray=dash,fillColor=getattr(style,'fillColor',color))
|
||||
elif self.joinedLines or lineStyle=='joinedLine':
|
||||
dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
|
||||
strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None))
|
||||
L = Line(x,yh2,x+width,yh2,strokeColor=color,strokeLineCap=0)
|
||||
if strokeWidth: L.strokeWidth = strokeWidth
|
||||
if dash: L.strokeDashArray = dash
|
||||
else:
|
||||
L = None
|
||||
|
||||
if hasattr(style, 'symbol'):
|
||||
S = style.symbol
|
||||
elif hasattr(baseStyle, 'symbol'):
|
||||
S = baseStyle.symbol
|
||||
else:
|
||||
S = None
|
||||
|
||||
if S: S = uSymbol2Symbol(S,x+width/2.,yh2,color)
|
||||
if S and L:
|
||||
g = Group()
|
||||
g.add(L)
|
||||
g.add(S)
|
||||
return g
|
||||
return S or L
|
||||
|
||||
def getSeriesName(self,i,default=None):
|
||||
'''return series name i or default'''
|
||||
return _objStr(getattr(self.lines[i],'name',default))
|
||||
|
||||
class LineChart(AbstractLineChart):
|
||||
pass
|
||||
|
||||
# This is conceptually similar to the VerticalBarChart.
|
||||
# Still it is better named HorizontalLineChart... :-/
|
||||
|
||||
class HorizontalLineChart(LineChart):
|
||||
"""Line chart with multiple lines.
|
||||
|
||||
A line chart is assumed to have one category and one value axis.
|
||||
Despite its generic name this particular line chart class has
|
||||
a vertical value axis and a horizontal category one. It may
|
||||
evolve into individual horizontal and vertical variants (like
|
||||
with the existing bar charts).
|
||||
|
||||
Available attributes are:
|
||||
|
||||
x: x-position of lower-left chart origin
|
||||
y: y-position of lower-left chart origin
|
||||
width: chart width
|
||||
height: chart height
|
||||
|
||||
useAbsolute: disables auto-scaling of chart elements (?)
|
||||
lineLabelNudge: distance of data labels to data points
|
||||
lineLabels: labels associated with data values
|
||||
lineLabelFormat: format string or callback function
|
||||
groupSpacing: space between categories
|
||||
|
||||
joinedLines: enables drawing of lines
|
||||
|
||||
strokeColor: color of chart lines (?)
|
||||
fillColor: color for chart background (?)
|
||||
lines: style list, used cyclically for data series
|
||||
|
||||
valueAxis: value axis object
|
||||
categoryAxis: category axis object
|
||||
categoryNames: category names
|
||||
|
||||
data: chart data, a list of data series of equal length
|
||||
"""
|
||||
_flipXY = 0
|
||||
|
||||
_attrMap = AttrMap(BASE=LineChart,
|
||||
useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.',advancedUsage=1),
|
||||
lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.',advancedUsage=1),
|
||||
lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'),
|
||||
lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'),
|
||||
lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'),
|
||||
groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'),
|
||||
joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'),
|
||||
lines = AttrMapValue(None, desc='Handle of the lines.'),
|
||||
valueAxis = AttrMapValue(None, desc='Handle of the value axis.'),
|
||||
categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'),
|
||||
categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
|
||||
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
|
||||
inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.',advancedUsage=1),
|
||||
reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.',advancedUsage=1),
|
||||
annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.',advancedUsage=1),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
LineChart.__init__(self)
|
||||
|
||||
# Allow for a bounding rectangle.
|
||||
self.strokeColor = None
|
||||
self.fillColor = None
|
||||
|
||||
# Named so we have less recoding for the horizontal one :-)
|
||||
if self._flipXY:
|
||||
self.categoryAxis = YCategoryAxis()
|
||||
self.valueAxis = XValueAxis()
|
||||
else:
|
||||
self.categoryAxis = XCategoryAxis()
|
||||
self.valueAxis = YValueAxis()
|
||||
|
||||
# This defines two series of 3 points. Just an example.
|
||||
self.data = [(100,110,120,130),
|
||||
(70, 80, 80, 90)]
|
||||
self.categoryNames = ('North','South','East','West')
|
||||
|
||||
self.lines = TypedPropertyCollection(LineChartProperties)
|
||||
self.lines.strokeWidth = 1
|
||||
self.lines[0].strokeColor = colors.red
|
||||
self.lines[1].strokeColor = colors.green
|
||||
self.lines[2].strokeColor = colors.blue
|
||||
|
||||
# control spacing. if useAbsolute = 1 then
|
||||
# the next parameters are in points; otherwise
|
||||
# they are 'proportions' and are normalized to
|
||||
# fit the available space.
|
||||
self.useAbsolute = 0 #- not done yet
|
||||
self.groupSpacing = 1 #5
|
||||
|
||||
self.lineLabels = TypedPropertyCollection(Label)
|
||||
self.lineLabelFormat = None
|
||||
self.lineLabelArray = None
|
||||
|
||||
# This says whether the origin is above or below
|
||||
# the data point. +10 means put the origin ten points
|
||||
# above the data point if value > 0, or ten
|
||||
# points below if data value < 0. This is different
|
||||
# to label dx/dy which are not dependent on the
|
||||
# sign of the data.
|
||||
self.lineLabelNudge = 10
|
||||
# If you have multiple series, by default they butt
|
||||
# together.
|
||||
|
||||
# New line chart attributes.
|
||||
self.joinedLines = 1 # Connect items with straight lines.
|
||||
self.inFill = 0
|
||||
self.reversePlotOrder = 0
|
||||
|
||||
def demo(self):
|
||||
"""Shows basic use of a line chart."""
|
||||
|
||||
drawing = Drawing(200, 100)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(14, 10, 21, 28, 38, 46, 25, 5)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 20
|
||||
lc.y = 10
|
||||
lc.height = 85
|
||||
lc.width = 170
|
||||
lc.data = data
|
||||
lc.lines.symbol = makeMarker('Circle')
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
def calcPositions(self):
|
||||
"""Works out where they go.
|
||||
|
||||
Sets an attribute _positions which is a list of
|
||||
lists of (x, y) matching the data.
|
||||
"""
|
||||
|
||||
self._seriesCount = len(self.data)
|
||||
self._rowLength = max(list(map(len,self.data)))
|
||||
|
||||
if self.useAbsolute:
|
||||
# Dimensions are absolute.
|
||||
normFactor = 1.0
|
||||
else:
|
||||
# Dimensions are normalized to fit.
|
||||
normWidth = self.groupSpacing
|
||||
availWidth = self.categoryAxis.scale(0)[1]
|
||||
normFactor = availWidth / normWidth
|
||||
self._normFactor = normFactor
|
||||
self._vzero = vzero = self.valueAxis.scale(0)
|
||||
self._hngs = hngs = 0.5 * self.groupSpacing * normFactor
|
||||
|
||||
pairs = set()
|
||||
P = [].append
|
||||
cscale = self.categoryAxis.scale
|
||||
vscale = self.valueAxis.scale
|
||||
data = self.data
|
||||
flipXY = self._flipXY
|
||||
n = len(data)
|
||||
for rowNo,row in enumerate(data):
|
||||
if isinstance(row, FillPairedData):
|
||||
other = row.other
|
||||
if 0<=other<n:
|
||||
if other==rowNo:
|
||||
raise ValueError('data row %r may not be paired with itself' % rowNo)
|
||||
t = (rowNo,other)
|
||||
pairs.add((min(t),max(t)))
|
||||
else:
|
||||
raise ValueError('data row %r is paired with invalid data row %r' % (rowNo, other))
|
||||
line = [].append
|
||||
for colNo,datum in enumerate(row):
|
||||
if datum is not None:
|
||||
c, g = cscale(colNo)
|
||||
v = vscale(datum)
|
||||
line((v, c+hngs) if flipXY else (c+hngs, v))
|
||||
P(line.__self__)
|
||||
P = P.__self__
|
||||
|
||||
#if there are some paired lines we ensure only one is created
|
||||
for rowNo, other in pairs:
|
||||
P[rowNo] = FillPairedData(P[rowNo],other)
|
||||
self._pairInFills = len(pairs)
|
||||
self._positions = P
|
||||
|
||||
def _innerDrawLabel(self, rowNo, colNo, x, y):
|
||||
"Draw a label for a given item in the list."
|
||||
|
||||
labelFmt = self.lineLabelFormat
|
||||
labelValue = self.data[rowNo][colNo]
|
||||
|
||||
if labelFmt is None:
|
||||
labelText = None
|
||||
elif type(labelFmt) is str:
|
||||
if labelFmt == 'values':
|
||||
try:
|
||||
labelText = self.lineLabelArray[rowNo][colNo]
|
||||
except:
|
||||
labelText = None
|
||||
else:
|
||||
labelText = labelFmt % labelValue
|
||||
elif hasattr(labelFmt,'__call__'):
|
||||
labelText = labelFmt(labelValue)
|
||||
else:
|
||||
raise ValueError("Unknown formatter type %s, expected string or function"%labelFmt)
|
||||
|
||||
if labelText:
|
||||
label = self.lineLabels[(rowNo, colNo)]
|
||||
if not label.visible: return
|
||||
# Make sure labels are some distance off the data point.
|
||||
if y > 0:
|
||||
label.setOrigin(x, y + self.lineLabelNudge)
|
||||
else:
|
||||
label.setOrigin(x, y - self.lineLabelNudge)
|
||||
label.setText(labelText)
|
||||
else:
|
||||
label = None
|
||||
return label
|
||||
|
||||
def drawLabel(self, G, rowNo, colNo, x, y):
|
||||
'''Draw a label for a given item in the list.
|
||||
G must have an add method'''
|
||||
G.add(self._innerDrawLabel(rowNo,colNo,x,y))
|
||||
|
||||
def makeLines(self):
|
||||
g = Group()
|
||||
|
||||
labelFmt = self.lineLabelFormat
|
||||
P = self._positions
|
||||
if self.reversePlotOrder: P.reverse()
|
||||
lines = self.lines
|
||||
styleCount = len(lines)
|
||||
flipXY = self._flipXY
|
||||
cA = self.categoryAxis
|
||||
vA = self.valueAxis
|
||||
_inFill = self.inFill
|
||||
if (_inFill or self._pairInFills or
|
||||
[rowNo for rowNo in range(len(P))
|
||||
if getattr(lines[rowNo%styleCount],'inFill',False)]
|
||||
):
|
||||
if flipXY:
|
||||
infillC = cA._x
|
||||
infillV0 = vA._y
|
||||
infillV1 = infillV0 + cA._length
|
||||
else:
|
||||
infillC = cA._y
|
||||
infillV0 = vA._x
|
||||
infillV1 = infillV0 + cA._length
|
||||
inFillG = getattr(self,'_inFillG',g)
|
||||
vzero = self._vzero
|
||||
bypos = None
|
||||
|
||||
# Iterate over data rows.
|
||||
for rowNo, row in enumerate(reversed(P) if self.reversePlotOrder else P):
|
||||
styleIdx = rowNo % styleCount
|
||||
rowStyle = lines[styleIdx]
|
||||
strokeColor = rowStyle.strokeColor
|
||||
fillColor = getattr(rowStyle,'fillColor',strokeColor)
|
||||
inFill = getattr(rowStyle,'inFill',_inFill)
|
||||
dash = getattr(rowStyle, 'strokeDashArray', None)
|
||||
lineStyle = getattr(rowStyle,'lineStyle',None)
|
||||
|
||||
if hasattr(rowStyle, 'strokeWidth'):
|
||||
strokeWidth = rowStyle.strokeWidth
|
||||
elif hasattr(lines, 'strokeWidth'):
|
||||
strokeWidth = lines.strokeWidth
|
||||
else:
|
||||
strokeWidth = None
|
||||
|
||||
# Iterate over data columns.
|
||||
if lineStyle=='bar':
|
||||
if bypos is None:
|
||||
if flipXY:
|
||||
bypos = max(vA._x,vzero)
|
||||
byneg = min(vA._x+vA._length,vzero)
|
||||
else:
|
||||
bypos = max(vA._y,vzero)
|
||||
byneg = min(vA._y+vA._length,vzero)
|
||||
barWidth = getattr(rowStyle,'barWidth',Percentage(50))
|
||||
if isinstance(barWidth,Percentage):
|
||||
hbw = self._hngs*barWidth*0.01
|
||||
else:
|
||||
hbw = barWidth*0.5
|
||||
for x, y in row:
|
||||
if flipXY:
|
||||
v0 = byneg if x<vzero else bypos
|
||||
t = v0, y-hbw, x-v0, 2*hbw
|
||||
else:
|
||||
v0 = byneg if y<vzero else bypos
|
||||
t = x-hbw,v0,2*hbw,y-v0
|
||||
g.add(Rect(*t,strokeWidth=strokeWidth,strokeColor=strokeColor,fillColor=fillColor))
|
||||
elif self.joinedLines or lineStyle=='joinedLine':
|
||||
points = flatten(row)
|
||||
if inFill or isinstance(row,FillPairedData):
|
||||
filler = getattr(rowStyle, 'filler', None)
|
||||
if isinstance(row,FillPairedData):
|
||||
fpoints = points + flatten(reversed(P[row.other]))
|
||||
else:
|
||||
if flipXY:
|
||||
fpoints = [infillC,infillV0] + points + [infillC,infillV1]
|
||||
else:
|
||||
fpoints = [infillV0,infillC] + points + [infillV1,infillC]
|
||||
if filler:
|
||||
filler.fill(self,inFillG,rowNo,fillColor,fpoints)
|
||||
else:
|
||||
inFillG.add(Polygon(fpoints,fillColor=fillColor,strokeColor=strokeColor if strokeColor==fillColor else None,strokeWidth=strokeWidth or 0.1))
|
||||
if not inFill or inFill==2 or strokeColor!=fillColor:
|
||||
line = PolyLine(points,strokeColor=strokeColor,strokeLineCap=0,strokeLineJoin=1)
|
||||
if strokeWidth:
|
||||
line.strokeWidth = strokeWidth
|
||||
if dash:
|
||||
line.strokeDashArray = dash
|
||||
g.add(line)
|
||||
|
||||
if hasattr(rowStyle, 'symbol'):
|
||||
uSymbol = rowStyle.symbol
|
||||
elif hasattr(lines, 'symbol'):
|
||||
uSymbol = lines.symbol
|
||||
else:
|
||||
uSymbol = None
|
||||
|
||||
if uSymbol:
|
||||
for colNo,(x,y) in enumerate(row):
|
||||
symbol = uSymbol2Symbol(tpcGetItem(uSymbol,colNo),x,y,rowStyle.strokeColor)
|
||||
if symbol: g.add(symbol)
|
||||
|
||||
# Draw item labels.
|
||||
for colNo, (x, y) in enumerate(row):
|
||||
self.drawLabel(g, rowNo, colNo, x, y)
|
||||
|
||||
return g
|
||||
|
||||
def draw(self):
|
||||
"Draws itself."
|
||||
|
||||
vA, cA = self.valueAxis, self.categoryAxis
|
||||
if self._flipXY:
|
||||
vA.setPosition(self.x, self.y, self.width)
|
||||
else:
|
||||
vA.setPosition(self.x, self.y, self.height)
|
||||
if vA: vA.joinAxis = cA
|
||||
if cA: cA.joinAxis = vA
|
||||
vA.configure(self.data)
|
||||
|
||||
y = self.y
|
||||
x = self.x
|
||||
if self._flipXY:
|
||||
# If zero is in chart, put y axis there, otherwise
|
||||
# use bottom.
|
||||
crossesAt = vA.scale(0)
|
||||
if not ((crossesAt > x + self.width) or (crossesAt < x)):
|
||||
x = crossesAt
|
||||
cA.setPosition(x, y, self.height)
|
||||
else:
|
||||
# If zero is in chart, put x axis there, otherwise
|
||||
# use bottom.
|
||||
crossesAt = vA.scale(0)
|
||||
if not ((crossesAt > y + self.height) or (crossesAt < y)):
|
||||
y = crossesAt
|
||||
cA.setPosition(x, y, self.width)
|
||||
cA.configure(self.data)
|
||||
|
||||
self.calcPositions()
|
||||
|
||||
g = Group()
|
||||
g.add(self.makeBackground())
|
||||
if self.inFill:
|
||||
self._inFillG = Group()
|
||||
g.add(self._inFillG)
|
||||
|
||||
g.add(cA)
|
||||
g.add(vA)
|
||||
cAdgl = getattr(cA,'drawGridLast',False)
|
||||
vAdgl = getattr(vA,'drawGridLast',False)
|
||||
if not cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
|
||||
if not vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
|
||||
g.add(self.makeLines())
|
||||
if cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
|
||||
if vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
|
||||
for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale))
|
||||
return g
|
||||
|
||||
def _fakeItemKey(a):
|
||||
'''t, z0, z1, x, y = a[:5]'''
|
||||
return (-a[1],a[3],a[0],-a[4])
|
||||
|
||||
class _FakeGroup:
|
||||
def __init__(self):
|
||||
self._data = []
|
||||
|
||||
def add(self,what):
|
||||
if what: self._data.append(what)
|
||||
|
||||
def value(self):
|
||||
return self._data
|
||||
|
||||
def sort(self):
|
||||
self._data.sort(key=_fakeItemKey)
|
||||
#for t in self._data: print t
|
||||
|
||||
class HorizontalLineChart3D(HorizontalLineChart):
|
||||
_attrMap = AttrMap(BASE=HorizontalLineChart,
|
||||
theta_x = AttrMapValue(isNumber, desc='dx/dz'),
|
||||
theta_y = AttrMapValue(isNumber, desc='dy/dz'),
|
||||
zDepth = AttrMapValue(isNumber, desc='depth of an individual series'),
|
||||
zSpace = AttrMapValue(isNumber, desc='z gap around series'),
|
||||
)
|
||||
theta_x = .5
|
||||
theta_y = .5
|
||||
zDepth = 10
|
||||
zSpace = 3
|
||||
|
||||
def calcPositions(self):
|
||||
HorizontalLineChart.calcPositions(self)
|
||||
nSeries = self._seriesCount
|
||||
zSpace = self.zSpace
|
||||
zDepth = self.zDepth
|
||||
if self.categoryAxis.style=='parallel_3d':
|
||||
_3d_depth = nSeries*zDepth+(nSeries+1)*zSpace
|
||||
else:
|
||||
_3d_depth = zDepth + 2*zSpace
|
||||
self._3d_dx = self.theta_x*_3d_depth
|
||||
self._3d_dy = self.theta_y*_3d_depth
|
||||
|
||||
def _calc_z0(self,rowNo):
|
||||
zSpace = self.zSpace
|
||||
if self.categoryAxis.style=='parallel_3d':
|
||||
z0 = rowNo*(self.zDepth+zSpace)+zSpace
|
||||
else:
|
||||
z0 = zSpace
|
||||
return z0
|
||||
|
||||
def _zadjust(self,x,y,z):
|
||||
return x+z*self.theta_x, y+z*self.theta_y
|
||||
|
||||
def makeLines(self):
|
||||
labelFmt = self.lineLabelFormat
|
||||
P = list(range(len(self._positions)))
|
||||
if self.reversePlotOrder: P.reverse()
|
||||
inFill = self.inFill
|
||||
assert not inFill, "inFill not supported for 3d yet"
|
||||
#if inFill:
|
||||
#inFillY = self.categoryAxis._y
|
||||
#inFillX0 = self.valueAxis._x
|
||||
#inFillX1 = inFillX0 + self.categoryAxis._length
|
||||
#inFillG = getattr(self,'_inFillG',g)
|
||||
zDepth = self.zDepth
|
||||
_zadjust = self._zadjust
|
||||
theta_x = self.theta_x
|
||||
theta_y = self.theta_y
|
||||
F = _FakeGroup()
|
||||
from reportlab.graphics.charts.utils3d import _make_3d_line_info
|
||||
tileWidth = getattr(self,'_3d_tilewidth',None)
|
||||
if not tileWidth and self.categoryAxis.style!='parallel_3d': tileWidth = 1
|
||||
|
||||
# Iterate over data rows.
|
||||
for rowNo in P:
|
||||
row = self._positions[rowNo]
|
||||
n = len(row)
|
||||
styleCount = len(self.lines)
|
||||
styleIdx = rowNo % styleCount
|
||||
rowStyle = self.lines[styleIdx]
|
||||
rowColor = rowStyle.strokeColor
|
||||
dash = getattr(rowStyle, 'strokeDashArray', None)
|
||||
z0 = self._calc_z0(rowNo)
|
||||
z1 = z0 + zDepth
|
||||
|
||||
if hasattr(self.lines[styleIdx], 'strokeWidth'):
|
||||
strokeWidth = self.lines[styleIdx].strokeWidth
|
||||
elif hasattr(self.lines, 'strokeWidth'):
|
||||
strokeWidth = self.lines.strokeWidth
|
||||
else:
|
||||
strokeWidth = None
|
||||
|
||||
# Iterate over data columns.
|
||||
if self.joinedLines:
|
||||
if n:
|
||||
x0, y0 = row[0]
|
||||
for colNo in range(1,n):
|
||||
x1, y1 = row[colNo]
|
||||
_make_3d_line_info( F, x0, x1, y0, y1, z0, z1,
|
||||
theta_x, theta_y,
|
||||
rowColor, fillColorShaded=None, tileWidth=tileWidth,
|
||||
strokeColor=None, strokeWidth=None, strokeDashArray=None,
|
||||
shading=0.1)
|
||||
x0, y0 = x1, y1
|
||||
|
||||
if hasattr(self.lines[styleIdx], 'symbol'):
|
||||
uSymbol = self.lines[styleIdx].symbol
|
||||
elif hasattr(self.lines, 'symbol'):
|
||||
uSymbol = self.lines.symbol
|
||||
else:
|
||||
uSymbol = None
|
||||
|
||||
if uSymbol:
|
||||
for colNo in range(n):
|
||||
x1, y1 = row[colNo]
|
||||
x1, y1 = _zadjust(x1,y1,z0)
|
||||
symbol = uSymbol2Symbol(uSymbol,x1,y1,rowColor)
|
||||
if symbol: F.add((2,z0,z0,x1,y1,symbol))
|
||||
|
||||
# Draw item labels.
|
||||
for colNo in range(n):
|
||||
x1, y1 = row[colNo]
|
||||
x1, y1 = _zadjust(x1,y1,z0)
|
||||
L = self._innerDrawLabel(rowNo, colNo, x1, y1)
|
||||
if L: F.add((2,z0,z0,x1,y1,L))
|
||||
|
||||
F.sort()
|
||||
g = Group()
|
||||
for v in F.value(): g.add(v[-1])
|
||||
return g
|
||||
|
||||
class VerticalLineChart(HorizontalLineChart):
|
||||
_flipXY = 1
|
||||
|
||||
def sample1():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.lines.symbol = makeMarker('FilledDiamond')
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
class SampleHorizontalLineChart(HorizontalLineChart):
|
||||
"Sample class overwriting one method to draw additional horizontal lines."
|
||||
|
||||
def demo(self):
|
||||
"""Shows basic use of a line chart."""
|
||||
|
||||
drawing = Drawing(200, 100)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(14, 10, 21, 28, 38, 46, 25, 5)
|
||||
]
|
||||
|
||||
lc = SampleHorizontalLineChart()
|
||||
|
||||
lc.x = 20
|
||||
lc.y = 10
|
||||
lc.height = 85
|
||||
lc.width = 170
|
||||
lc.data = data
|
||||
lc.strokeColor = colors.white
|
||||
lc.fillColor = colors.HexColor(0xCCCCCC)
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
def makeBackground(self):
|
||||
g = Group()
|
||||
|
||||
g.add(HorizontalLineChart.makeBackground(self))
|
||||
|
||||
valAxis = self.valueAxis
|
||||
valTickPositions = valAxis._tickValues
|
||||
|
||||
for y in valTickPositions:
|
||||
y = valAxis.scale(y)
|
||||
g.add(Line(self.x, y, self.x+self.width, y,
|
||||
strokeColor = self.strokeColor))
|
||||
|
||||
return g
|
||||
|
||||
def sample1a():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = SampleHorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.strokeColor = colors.white
|
||||
lc.fillColor = colors.HexColor(0xCCCCCC)
|
||||
lc.lines.symbol = makeMarker('FilledDiamond')
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
def sample2():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.lines.symbol = makeMarker('Smiley')
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
lc.strokeColor = colors.black
|
||||
lc.fillColor = colors.lightblue
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
def sample3():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
lc.strokeColor = colors.black
|
||||
|
||||
lc.lines[0].symbol = makeMarker('Smiley')
|
||||
lc.lines[1].symbol = NoEntry
|
||||
lc.lines[0].strokeWidth = 2
|
||||
lc.lines[1].strokeWidth = 4
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
def sampleCandleStick():
|
||||
from reportlab.graphics.widgetbase import CandleSticks
|
||||
d = Drawing(400, 200)
|
||||
chart = HorizontalLineChart()
|
||||
d.add(chart)
|
||||
chart.y = 20
|
||||
boxMid = (100, 110, 120, 130)
|
||||
hi = [m+10 for m in boxMid]
|
||||
lo = [m-10 for m in boxMid]
|
||||
boxHi = [m+6 for m in boxMid]
|
||||
boxLo = [m-4 for m in boxMid]
|
||||
boxFillColor = colors.pink
|
||||
boxWidth = 20
|
||||
crossWidth = 10
|
||||
candleStrokeWidth = 0.5
|
||||
candleStrokeColor = colors.black
|
||||
chart.valueAxis.avoidBoundSpace = 5
|
||||
|
||||
chart.valueAxis.valueMin = min(min(boxMid),min(hi),min(lo),min(boxLo),min(boxHi))
|
||||
chart.valueAxis.valueMax = max(max(boxMid),max(hi),max(lo),max(boxLo),max(boxHi))
|
||||
lines = chart.lines
|
||||
lines[0].strokeColor = None
|
||||
I = range(len(boxMid))
|
||||
chart.data = [boxMid]
|
||||
lines[0].symbol = candles = CandleSticks(chart=chart, boxFillColor=boxFillColor, boxWidth=boxWidth, crossWidth=crossWidth, strokeWidth=candleStrokeWidth, strokeColor=candleStrokeColor)
|
||||
for i in I: candles[i].setProperties(dict(position=i,boxMid=boxMid[i],crossLo=lo[i],crossHi=hi[i],boxLo=boxLo[i],boxHi=boxHi[i]))
|
||||
return d
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/markers.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""This modules defines a collection of markers used in charts.
|
||||
|
||||
The make* functions return a simple shape or a widget as for
|
||||
the smiley.
|
||||
"""
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.graphics.shapes import Rect, Circle, Polygon
|
||||
from reportlab.graphics.widgets.signsandsymbols import SmileyFace
|
||||
|
||||
|
||||
def makeEmptySquare(x, y, size, color):
|
||||
"Make an empty square marker."
|
||||
|
||||
d = size/2.0
|
||||
rect = Rect(x-d, y-d, 2*d, 2*d)
|
||||
rect.strokeColor = color
|
||||
rect.fillColor = None
|
||||
|
||||
return rect
|
||||
|
||||
|
||||
def makeFilledSquare(x, y, size, color):
|
||||
"Make a filled square marker."
|
||||
|
||||
d = size/2.0
|
||||
rect = Rect(x-d, y-d, 2*d, 2*d)
|
||||
rect.strokeColor = color
|
||||
rect.fillColor = color
|
||||
|
||||
return rect
|
||||
|
||||
|
||||
def makeFilledDiamond(x, y, size, color):
|
||||
"Make a filled diamond marker."
|
||||
|
||||
d = size/2.0
|
||||
poly = Polygon((x-d,y, x,y+d, x+d,y, x,y-d))
|
||||
poly.strokeColor = color
|
||||
poly.fillColor = color
|
||||
|
||||
return poly
|
||||
|
||||
|
||||
def makeEmptyCircle(x, y, size, color):
|
||||
"Make a hollow circle marker."
|
||||
|
||||
d = size/2.0
|
||||
circle = Circle(x, y, d)
|
||||
circle.strokeColor = color
|
||||
circle.fillColor = colors.white
|
||||
|
||||
return circle
|
||||
|
||||
|
||||
def makeFilledCircle(x, y, size, color):
|
||||
"Make a hollow circle marker."
|
||||
|
||||
d = size/2.0
|
||||
circle = Circle(x, y, d)
|
||||
circle.strokeColor = color
|
||||
circle.fillColor = color
|
||||
|
||||
return circle
|
||||
|
||||
|
||||
def makeSmiley(x, y, size, color):
|
||||
"Make a smiley marker."
|
||||
|
||||
d = size
|
||||
s = SmileyFace()
|
||||
s.fillColor = color
|
||||
s.x = x-d
|
||||
s.y = y-d
|
||||
s.size = d*2
|
||||
|
||||
return s
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.colors import black, white
|
||||
from reportlab.graphics.shapes import Polygon, String, Drawing, Group, Rect
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.lib.validators import *
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.pdfbase.pdfmetrics import getFont
|
||||
from reportlab.graphics.widgets.grids import ShadedRect
|
||||
|
||||
class SlideBox(Widget):
|
||||
"""Returns a slidebox widget"""
|
||||
_attrMap = AttrMap(
|
||||
labelFontName = AttrMapValue(isString, desc="Name of font used for the labels"),
|
||||
labelFontSize = AttrMapValue(isNumber, desc="Size of font used for the labels"),
|
||||
labelStrokeColor = AttrMapValue(isColorOrNone, desc="Colour for for number outlines"),
|
||||
labelFillColor = AttrMapValue(isColorOrNone, desc="Colour for number insides"),
|
||||
startColor = AttrMapValue(isColor, desc='Color of first box'),
|
||||
endColor = AttrMapValue(isColor, desc='Color of last box'),
|
||||
numberOfBoxes = AttrMapValue(isInt, desc='How many boxes there are'),
|
||||
trianglePosition = AttrMapValue(isInt, desc='Which box is highlighted by the triangles'),
|
||||
triangleHeight = AttrMapValue(isNumber, desc="Height of indicator triangles"),
|
||||
triangleWidth = AttrMapValue(isNumber, desc="Width of indicator triangles"),
|
||||
triangleFillColor = AttrMapValue(isColor, desc="Colour of indicator triangles"),
|
||||
triangleStrokeColor = AttrMapValue(isColorOrNone, desc="Colour of indicator triangle outline"),
|
||||
triangleStrokeWidth = AttrMapValue(isNumber, desc="Colour of indicator triangle outline"),
|
||||
boxHeight = AttrMapValue(isNumber, desc="Height of the boxes"),
|
||||
boxWidth = AttrMapValue(isNumber, desc="Width of the boxes"),
|
||||
boxSpacing = AttrMapValue(isNumber, desc="Space between the boxes"),
|
||||
boxOutlineColor = AttrMapValue(isColorOrNone, desc="Colour used to outline the boxes (if any)"),
|
||||
boxOutlineWidth = AttrMapValue(isNumberOrNone, desc="Width of the box outline (if any)"),
|
||||
leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'),
|
||||
rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'),
|
||||
topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'),
|
||||
bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'),
|
||||
background = AttrMapValue(isColorOrNone, desc='Colour of the background to the drawing (if any)'),
|
||||
sourceLabelText = AttrMapValue(isNoneOrString, desc="Text used for the 'source' label (can be empty)"),
|
||||
sourceLabelOffset = AttrMapValue(isNumber, desc='Padding at bottom of drawing'),
|
||||
sourceLabelFontName = AttrMapValue(isString, desc="Name of font used for the 'source' label"),
|
||||
sourceLabelFontSize = AttrMapValue(isNumber, desc="Font size for the 'source' label"),
|
||||
sourceLabelFillColor = AttrMapValue(isColorOrNone, desc="Colour ink for the 'source' label (bottom right)"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.labelFontName = "Helvetica-Bold"
|
||||
self.labelFontSize = 10
|
||||
self.labelStrokeColor = black
|
||||
self.labelFillColor = white
|
||||
self.startColor = colors.Color(232/255.0,224/255.0,119/255.0)
|
||||
self.endColor = colors.Color(25/255.0,77/255.0,135/255.0)
|
||||
self.numberOfBoxes = 7
|
||||
self.trianglePosition = 7
|
||||
self.triangleHeight = 0.12*cm
|
||||
self.triangleWidth = 0.38*cm
|
||||
self.triangleFillColor = white
|
||||
self.triangleStrokeColor = black
|
||||
self.triangleStrokeWidth = 0.58
|
||||
self.boxHeight = 0.55*cm
|
||||
self.boxWidth = 0.73*cm
|
||||
self.boxSpacing = 0.075*cm
|
||||
self.boxOutlineColor = black
|
||||
self.boxOutlineWidth = 0.58
|
||||
self.leftPadding=5
|
||||
self.rightPadding=5
|
||||
self.topPadding=5
|
||||
self.bottomPadding=5
|
||||
self.background=None
|
||||
self.sourceLabelText = "Source: ReportLab"
|
||||
self.sourceLabelOffset = 0.2*cm
|
||||
self.sourceLabelFontName = "Helvetica-Oblique"
|
||||
self.sourceLabelFontSize = 6
|
||||
self.sourceLabelFillColor = black
|
||||
|
||||
def _getDrawingDimensions(self):
|
||||
tx=(self.numberOfBoxes*self.boxWidth)
|
||||
if self.numberOfBoxes>1: tx=tx+((self.numberOfBoxes-1)*self.boxSpacing)
|
||||
tx=tx+self.leftPadding+self.rightPadding
|
||||
ty=self.boxHeight+self.triangleHeight
|
||||
ty=ty+self.topPadding+self.bottomPadding+self.sourceLabelOffset+self.sourceLabelFontSize
|
||||
return (tx,ty)
|
||||
|
||||
def _getColors(self):
|
||||
# for calculating intermediate colors...
|
||||
numShades = self.numberOfBoxes+1
|
||||
fillColorStart = self.startColor
|
||||
fillColorEnd = self.endColor
|
||||
colorsList =[]
|
||||
|
||||
for i in range(0,numShades):
|
||||
colorsList.append(colors.linearlyInterpolatedColor(fillColorStart, fillColorEnd, 0, numShades-1, i))
|
||||
return colorsList
|
||||
|
||||
def demo(self,drawing=None):
|
||||
if not drawing:
|
||||
tx,ty=self._getDrawingDimensions()
|
||||
drawing = Drawing(tx,ty)
|
||||
drawing.add(self.draw())
|
||||
return drawing
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
ys = self.bottomPadding+(self.triangleHeight/2)+self.sourceLabelOffset+self.sourceLabelFontSize
|
||||
if self.background:
|
||||
x,y = self._getDrawingDimensions()
|
||||
g.add(Rect(-self.leftPadding,-ys,x,y,
|
||||
strokeColor=None,
|
||||
strokeWidth=0,
|
||||
fillColor=self.background))
|
||||
|
||||
ascent=getFont(self.labelFontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent=ascent*self.labelFontSize # normalize
|
||||
|
||||
colorsList = self._getColors()
|
||||
|
||||
# Draw the boxes - now uses ShadedRect from grids
|
||||
x=0
|
||||
for f in range (0,self.numberOfBoxes):
|
||||
sr=ShadedRect()
|
||||
sr.x=x
|
||||
sr.y=0
|
||||
sr.width=self.boxWidth
|
||||
sr.height=self.boxHeight
|
||||
sr.orientation = 'vertical'
|
||||
sr.numShades = 30
|
||||
sr.fillColorStart = colorsList[f]
|
||||
sr.fillColorEnd = colorsList[f+1]
|
||||
sr.strokeColor = None
|
||||
sr.strokeWidth = 0
|
||||
|
||||
g.add(sr)
|
||||
|
||||
g.add(Rect(x,0,self.boxWidth,self.boxHeight,
|
||||
strokeColor=self.boxOutlineColor,
|
||||
strokeWidth=self.boxOutlineWidth,
|
||||
fillColor=None))
|
||||
|
||||
g.add(String(x+self.boxWidth/2.,(self.boxHeight-ascent)/2.,
|
||||
text = str(f+1),
|
||||
fillColor = self.labelFillColor,
|
||||
strokeColor=self.labelStrokeColor,
|
||||
textAnchor = 'middle',
|
||||
fontName = self.labelFontName,
|
||||
fontSize = self.labelFontSize))
|
||||
x=x+self.boxWidth+self.boxSpacing
|
||||
|
||||
#do triangles
|
||||
xt = (self.trianglePosition*self.boxWidth)
|
||||
if self.trianglePosition>1:
|
||||
xt = xt+(self.trianglePosition-1)*self.boxSpacing
|
||||
xt = xt-(self.boxWidth/2)
|
||||
g.add(Polygon(
|
||||
strokeColor = self.triangleStrokeColor,
|
||||
strokeWidth = self.triangleStrokeWidth,
|
||||
fillColor = self.triangleFillColor,
|
||||
points=[xt,self.boxHeight-(self.triangleHeight/2),
|
||||
xt-(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2),
|
||||
xt+(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2),
|
||||
xt,self.boxHeight-(self.triangleHeight/2)]))
|
||||
g.add(Polygon(
|
||||
strokeColor = self.triangleStrokeColor,
|
||||
strokeWidth = self.triangleStrokeWidth,
|
||||
fillColor = self.triangleFillColor,
|
||||
points=[xt,0+(self.triangleHeight/2),
|
||||
xt-(self.triangleWidth/2),0-(self.triangleHeight/2),
|
||||
xt+(self.triangleWidth/2),0-(self.triangleHeight/2),
|
||||
xt,0+(self.triangleHeight/2)]))
|
||||
|
||||
#source label
|
||||
if self.sourceLabelText != None:
|
||||
g.add(String(x-self.boxSpacing,0-(self.triangleHeight/2)-self.sourceLabelOffset-(self.sourceLabelFontSize),
|
||||
text = self.sourceLabelText,
|
||||
fillColor = self.sourceLabelFillColor,
|
||||
textAnchor = 'end',
|
||||
fontName = self.sourceLabelFontName,
|
||||
fontSize = self.sourceLabelFontSize))
|
||||
|
||||
g.shift(self.leftPadding, ys)
|
||||
|
||||
return g
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
d = SlideBox()
|
||||
d.demo().save(fnRoot="slidebox")
|
||||
@@ -0,0 +1,411 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/spider.py
|
||||
# spider chart, also known as radar chart
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""Spider Chart
|
||||
|
||||
Normal use shows variation of 5-10 parameters against some 'norm' or target.
|
||||
When there is more than one series, place the series with the largest
|
||||
numbers first, as it will be overdrawn by each successive one.
|
||||
"""
|
||||
|
||||
from math import sin, cos, pi
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isListOfNumbersOrNone,\
|
||||
isColorOrNone, isListOfStringsOrNone, OneOf,\
|
||||
isBoolean, isNumberOrNone,\
|
||||
isStringOrNone, isStringOrNone, EitherOr,\
|
||||
isCallable, NoneOr
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, PolyLine, \
|
||||
STATE_DEFAULTS
|
||||
from reportlab.graphics.widgetbase import TypedPropertyCollection, PropHolder
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
from reportlab.graphics.charts.legends import _objStr
|
||||
from reportlab.graphics.charts.piecharts import WedgeLabel
|
||||
from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol, isSymbol
|
||||
|
||||
class StrandProperty(PropHolder):
|
||||
|
||||
_attrMap = AttrMap(
|
||||
strokeWidth = AttrMapValue(isNumber,desc='width'),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc='filling color'),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc='stroke color'),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc='dashing pattern, e.g. (3,2)'),
|
||||
symbol = AttrMapValue(EitherOr((isStringOrNone,isSymbol)), desc='Widget placed at data points.',advancedUsage=1),
|
||||
symbolSize= AttrMapValue(isNumber, desc='Symbol size.',advancedUsage=1),
|
||||
name = AttrMapValue(isStringOrNone, desc='Name of the strand.'),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.strokeWidth = 1
|
||||
self.fillColor = None
|
||||
self.strokeColor = STATE_DEFAULTS["strokeColor"]
|
||||
self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
|
||||
self.symbol = None
|
||||
self.symbolSize = 5
|
||||
self.name = None
|
||||
|
||||
class SpokeProperty(PropHolder):
|
||||
_attrMap = AttrMap(
|
||||
strokeWidth = AttrMapValue(isNumber,desc='width'),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc='filling color'),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc='stroke color'),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc='dashing pattern, e.g. (2,1)'),
|
||||
labelRadius = AttrMapValue(isNumber,desc='label radius',advancedUsage=1),
|
||||
visible = AttrMapValue(isBoolean,desc="True if the spoke line is to be drawn"),
|
||||
)
|
||||
|
||||
def __init__(self,**kw):
|
||||
self.strokeWidth = 0.5
|
||||
self.fillColor = None
|
||||
self.strokeColor = STATE_DEFAULTS["strokeColor"]
|
||||
self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
|
||||
self.visible = 1
|
||||
self.labelRadius = 1.05
|
||||
|
||||
class SpokeLabel(WedgeLabel):
|
||||
def __init__(self,**kw):
|
||||
WedgeLabel.__init__(self,**kw)
|
||||
if '_text' not in list(kw.keys()): self._text = ''
|
||||
|
||||
class StrandLabel(SpokeLabel):
|
||||
_attrMap = AttrMap(BASE=SpokeLabel,
|
||||
format = AttrMapValue(EitherOr((isStringOrNone,isCallable)),desc="Format for the label"),
|
||||
dR = AttrMapValue(isNumberOrNone,desc="radial shift for label"),
|
||||
)
|
||||
def __init__(self,**kw):
|
||||
self.format = ''
|
||||
self.dR = 0
|
||||
SpokeLabel.__init__(self,**kw)
|
||||
|
||||
def _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty):
|
||||
L = labelClass()
|
||||
L._text = text
|
||||
L.x = cx + radius*car
|
||||
L.y = cy + radius*sar
|
||||
L._pmv = angle*180/pi
|
||||
L.boxAnchor = sty.boxAnchor
|
||||
L.dx = sty.dx
|
||||
L.dy = sty.dy
|
||||
L.angle = sty.angle
|
||||
L.boxAnchor = sty.boxAnchor
|
||||
L.boxStrokeColor = sty.boxStrokeColor
|
||||
L.boxStrokeWidth = sty.boxStrokeWidth
|
||||
L.boxFillColor = sty.boxFillColor
|
||||
L.strokeColor = sty.strokeColor
|
||||
L.strokeWidth = sty.strokeWidth
|
||||
L.leading = sty.leading
|
||||
L.width = sty.width
|
||||
L.maxWidth = sty.maxWidth
|
||||
L.height = sty.height
|
||||
L.textAnchor = sty.textAnchor
|
||||
L.visible = sty.visible
|
||||
L.topPadding = sty.topPadding
|
||||
L.leftPadding = sty.leftPadding
|
||||
L.rightPadding = sty.rightPadding
|
||||
L.bottomPadding = sty.bottomPadding
|
||||
L.fontName = sty.fontName
|
||||
L.fontSize = sty.fontSize
|
||||
L.fillColor = sty.fillColor
|
||||
return L
|
||||
|
||||
class SpiderChart(PlotArea):
|
||||
_attrMap = AttrMap(BASE=PlotArea,
|
||||
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
|
||||
labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
|
||||
startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
|
||||
direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
|
||||
strands = AttrMapValue(None, desc="collection of strand descriptor objects"),
|
||||
spokes = AttrMapValue(None, desc="collection of spoke descriptor objects"),
|
||||
strandLabels = AttrMapValue(None, desc="collection of strand label descriptor objects"),
|
||||
strandLabelClass=AttrMapValue(NoneOr(isCallable), desc="A class factory to use for the strand labels"),
|
||||
spokeLabels = AttrMapValue(None, desc="collection of spoke label descriptor objects"),
|
||||
spokeLabelClass=AttrMapValue(NoneOr(isCallable), desc="A class factory to use for the spoke labels"),
|
||||
)
|
||||
|
||||
def makeSwatchSample(self, rowNo, x, y, width, height):
|
||||
baseStyle = self.strands
|
||||
styleIdx = rowNo % len(baseStyle)
|
||||
style = baseStyle[styleIdx]
|
||||
strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None))
|
||||
fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None))
|
||||
strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
|
||||
strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',0))
|
||||
symbol = getattr(style, 'symbol', getattr(baseStyle, 'symbol',None))
|
||||
ym = y+height/2.0
|
||||
if fillColor is None and strokeColor is not None and strokeWidth>0:
|
||||
bg = Line(x,ym,x+width,ym,strokeWidth=strokeWidth,strokeColor=strokeColor,
|
||||
strokeDashArray=strokeDashArray)
|
||||
elif fillColor is not None:
|
||||
bg = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor,
|
||||
strokeDashArray=strokeDashArray,fillColor=fillColor)
|
||||
else:
|
||||
bg = None
|
||||
if symbol:
|
||||
symbol = uSymbol2Symbol(symbol,x+width/2.,ym,color)
|
||||
if bg:
|
||||
g = Group()
|
||||
g.add(bg)
|
||||
g.add(symbol)
|
||||
return g
|
||||
return symbol or bg
|
||||
|
||||
def getSeriesName(self,i,default=None):
|
||||
'''return series name i or default'''
|
||||
return _objStr(getattr(self.strands[i],'name',default))
|
||||
|
||||
def __init__(self):
|
||||
PlotArea.__init__(self)
|
||||
|
||||
self.data = [[10,12,14,16,14,12], [6,8,10,12,9,11]]
|
||||
self.labels = None # or list of strings
|
||||
self.labels = ['a','b','c','d','e','f']
|
||||
self.startAngle = 90
|
||||
self.direction = "clockwise"
|
||||
|
||||
self.strands = TypedPropertyCollection(StrandProperty)
|
||||
self.spokes = TypedPropertyCollection(SpokeProperty)
|
||||
self.spokeLabels = TypedPropertyCollection(SpokeLabel)
|
||||
self.spokeLabels._text = None
|
||||
self.strandLabels = TypedPropertyCollection(StrandLabel)
|
||||
self.x = 10
|
||||
self.y = 10
|
||||
self.width = 180
|
||||
self.height = 180
|
||||
|
||||
def demo(self):
|
||||
d = Drawing(200, 200)
|
||||
d.add(SpiderChart())
|
||||
return d
|
||||
|
||||
def normalizeData(self, outer = 0.0):
|
||||
"""Turns data into normalized ones where each datum is < 1.0,
|
||||
and 1.0 = maximum radius. Adds 10% at outside edge by default"""
|
||||
data = self.data
|
||||
assert min(list(map(min,data))) >=0, "Cannot do spider plots of negative numbers!"
|
||||
norm = max(list(map(max,data)))
|
||||
norm *= (1.0+outer)
|
||||
if norm<1e-9: norm = 1.0
|
||||
self._norm = norm
|
||||
return [[e/norm for e in row] for row in data]
|
||||
|
||||
def _innerDrawLabel(self, sty, radius, cx, cy, angle, car, sar, labelClass=None):
|
||||
"Draw a label for a given item in the list."
|
||||
fmt = sty.format
|
||||
value = radius*self._norm
|
||||
if not fmt:
|
||||
text = None
|
||||
elif isinstance(fmt,str):
|
||||
if fmt == 'values':
|
||||
text = sty._text
|
||||
else:
|
||||
text = fmt % value
|
||||
elif hasattr(fmt,'__call__'):
|
||||
text = fmt(value)
|
||||
else:
|
||||
raise ValueError("Unknown formatter type %s, expected string or function" % fmt)
|
||||
|
||||
if text:
|
||||
dR = sty.dR
|
||||
if dR:
|
||||
radius += dR/self._radius
|
||||
L = _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty)
|
||||
if dR<0: L._anti = 1
|
||||
else:
|
||||
L = None
|
||||
return L
|
||||
|
||||
def labelClass(self,kind):
|
||||
klass = getattr(self,f'{kind}LabelClass',None)
|
||||
if not klass:
|
||||
klass = globals()[f'{kind.capitalize()}Label']
|
||||
return klass
|
||||
|
||||
def draw(self):
|
||||
# normalize slice data
|
||||
g = self.makeBackground() or Group()
|
||||
|
||||
xradius = self.width/2.0
|
||||
yradius = self.height/2.0
|
||||
self._radius = radius = min(xradius, yradius)
|
||||
cx = self.x + xradius
|
||||
cy = self.y + yradius
|
||||
|
||||
data = self.normalizeData()
|
||||
|
||||
self._seriesCount = len(data)
|
||||
n = len(data[0])
|
||||
|
||||
#labels
|
||||
if self.labels is None:
|
||||
labels = [''] * n
|
||||
else:
|
||||
labels = self.labels
|
||||
#there's no point in raising errors for less than enough errors if
|
||||
#we silently create all for the extreme case of no labels.
|
||||
i = n-len(labels)
|
||||
if i>0:
|
||||
labels = labels + ['']*i
|
||||
|
||||
S = []
|
||||
STRANDS = []
|
||||
STRANDAREAS = []
|
||||
syms = []
|
||||
labs = []
|
||||
csa = []
|
||||
angle = self.startAngle*pi/180
|
||||
direction = self.direction == "clockwise" and -1 or 1
|
||||
angleBetween = direction*(2 * pi)/float(n)
|
||||
spokes = self.spokes
|
||||
spokeLabels = self.spokeLabels
|
||||
for i in range(n):
|
||||
car = cos(angle)*radius
|
||||
sar = sin(angle)*radius
|
||||
csa.append((car,sar,angle))
|
||||
si = self.spokes[i]
|
||||
if si.visible:
|
||||
spoke = Line(cx, cy, cx + car, cy + sar, strokeWidth = si.strokeWidth, strokeColor=si.strokeColor, strokeDashArray=si.strokeDashArray)
|
||||
S.append(spoke)
|
||||
sli = spokeLabels[i]
|
||||
text = sli._text
|
||||
if not text: text = labels[i]
|
||||
if text:
|
||||
S.append(_setupLabel(self.labelClass('spoke'), text, si.labelRadius, cx, cy, angle, car, sar, sli))
|
||||
angle += angleBetween
|
||||
|
||||
# now plot the polygons
|
||||
rowIdx = 0
|
||||
strands = self.strands
|
||||
strandLabels = self.strandLabels
|
||||
for row in data:
|
||||
# series plot
|
||||
rsty = strands[rowIdx]
|
||||
points = []
|
||||
car, sar = csa[-1][:2]
|
||||
r = row[-1]
|
||||
points.append(cx+car*r)
|
||||
points.append(cy+sar*r)
|
||||
for i in range(n):
|
||||
car, sar, angle = csa[i]
|
||||
r = row[i]
|
||||
points.append(cx+car*r)
|
||||
points.append(cy+sar*r)
|
||||
L = self._innerDrawLabel(strandLabels[(rowIdx,i)], r, cx, cy, angle, car, sar, labelClass=self.labelClass('strand'))
|
||||
if L: labs.append(L)
|
||||
sty = strands[(rowIdx,i)]
|
||||
uSymbol = sty.symbol
|
||||
|
||||
# put in a marker, if it needs one
|
||||
if uSymbol:
|
||||
s_x = cx+car*r
|
||||
s_y = cy+sar*r
|
||||
s_fillColor = sty.fillColor
|
||||
s_strokeColor = sty.strokeColor
|
||||
s_strokeWidth = sty.strokeWidth
|
||||
s_angle = 0
|
||||
s_size = sty.symbolSize
|
||||
if type(uSymbol) is type(''):
|
||||
symbol = makeMarker(uSymbol,
|
||||
size = s_size,
|
||||
x = s_x,
|
||||
y = s_y,
|
||||
fillColor = s_fillColor,
|
||||
strokeColor = s_strokeColor,
|
||||
strokeWidth = s_strokeWidth,
|
||||
angle = s_angle,
|
||||
)
|
||||
else:
|
||||
symbol = uSymbol2Symbol(uSymbol,s_x,s_y,s_fillColor)
|
||||
for k,v in (('size', s_size), ('fillColor', s_fillColor),
|
||||
('x', s_x), ('y', s_y),
|
||||
('strokeColor',s_strokeColor), ('strokeWidth',s_strokeWidth),
|
||||
('angle',s_angle),):
|
||||
if getattr(symbol,k,None) is None:
|
||||
try:
|
||||
setattr(symbol,k,v)
|
||||
except:
|
||||
pass
|
||||
syms.append(symbol)
|
||||
|
||||
# make up the 'strand'
|
||||
if rsty.fillColor:
|
||||
strand = Polygon(points)
|
||||
strand.fillColor = rsty.fillColor
|
||||
strand.strokeColor = None
|
||||
strand.strokeWidth = 0
|
||||
STRANDAREAS.append(strand)
|
||||
if rsty.strokeColor and rsty.strokeWidth:
|
||||
strand = PolyLine(points)
|
||||
strand.strokeColor = rsty.strokeColor
|
||||
strand.strokeWidth = rsty.strokeWidth
|
||||
strand.strokeDashArray = rsty.strokeDashArray
|
||||
STRANDS.append(strand)
|
||||
rowIdx += 1
|
||||
|
||||
for s in (STRANDAREAS+STRANDS+syms+S+labs): g.add(s)
|
||||
return g
|
||||
|
||||
def sample1():
|
||||
"Make a simple spider chart"
|
||||
d = Drawing(400, 400)
|
||||
sp = SpiderChart()
|
||||
sp.x = 50
|
||||
sp.y = 50
|
||||
sp.width = 300
|
||||
sp.height = 300
|
||||
sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]]
|
||||
sp.labels = ['a','b','c','d','e','f']
|
||||
sp.strands[0].strokeColor = colors.cornsilk
|
||||
sp.strands[1].strokeColor = colors.cyan
|
||||
sp.strands[2].strokeColor = colors.palegreen
|
||||
sp.strands[0].fillColor = colors.cornsilk
|
||||
sp.strands[1].fillColor = colors.cyan
|
||||
sp.strands[2].fillColor = colors.palegreen
|
||||
sp.spokes.strokeDashArray = (2,2)
|
||||
d.add(sp)
|
||||
return d
|
||||
|
||||
|
||||
def sample2():
|
||||
"Make a spider chart with markers, but no fill"
|
||||
d = Drawing(400, 400)
|
||||
sp = SpiderChart()
|
||||
sp.x = 50
|
||||
sp.y = 50
|
||||
sp.width = 300
|
||||
sp.height = 300
|
||||
sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]]
|
||||
sp.labels = ['U','V','W','X','Y','Z']
|
||||
sp.strands.strokeWidth = 1
|
||||
sp.strands[0].fillColor = colors.pink
|
||||
sp.strands[1].fillColor = colors.lightblue
|
||||
sp.strands[2].fillColor = colors.palegreen
|
||||
sp.strands[0].strokeColor = colors.red
|
||||
sp.strands[1].strokeColor = colors.blue
|
||||
sp.strands[2].strokeColor = colors.green
|
||||
sp.strands.symbol = "FilledDiamond"
|
||||
sp.strands[1].symbol = makeMarker("Circle")
|
||||
sp.strands[1].symbol.strokeWidth = 0.5
|
||||
sp.strands[1].symbol.fillColor = colors.yellow
|
||||
sp.strands.symbolSize = 6
|
||||
sp.strandLabels[0,3]._text = 'special'
|
||||
sp.strandLabels[0,1]._text = 'one'
|
||||
sp.strandLabels[0,0]._text = 'zero'
|
||||
sp.strandLabels[1,0]._text = 'Earth'
|
||||
sp.strandLabels[2,2]._text = 'Mars'
|
||||
sp.strandLabels.format = 'values'
|
||||
sp.strandLabels.dR = -5
|
||||
d.add(sp)
|
||||
return d
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
d = sample1()
|
||||
from reportlab.graphics.renderPDF import drawToFile
|
||||
drawToFile(d, 'spider.pdf')
|
||||
d = sample2()
|
||||
drawToFile(d, 'spider2.pdf')
|
||||
@@ -0,0 +1,581 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/textlabels.py
|
||||
__version__='3.3.0'
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.utils import simpleSplit
|
||||
from reportlab.lib.geomutils import normalizeTRBL
|
||||
from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \
|
||||
isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString, isNoneOrCallable, \
|
||||
isSubclassOf, EitherOr, isListOfNumbers
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth, getAscentDescent
|
||||
from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS
|
||||
from reportlab.graphics.widgetbase import Widget, PropHolder
|
||||
from reportlab.graphics.shapes import DirectDraw
|
||||
from reportlab.platypus import XPreformatted, Flowable
|
||||
from reportlab.lib.styles import ParagraphStyle, PropertySet
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER
|
||||
_ta2al = dict(start=TA_LEFT,end=TA_RIGHT,middle=TA_CENTER)
|
||||
from ..utils import text2Path as _text2Path #here for continuity
|
||||
|
||||
_A2BA= {
|
||||
'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'},
|
||||
'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'},
|
||||
}
|
||||
|
||||
try:
|
||||
from rlextra.graphics.canvasadapter import DirectDrawFlowable
|
||||
except ImportError:
|
||||
DirectDrawFlowable = None
|
||||
|
||||
_BA2TA={'w':'start','nw':'start','sw':'start','e':'end', 'ne': 'end', 'se':'end', 'n':'middle','s':'middle','c':'middle'}
|
||||
class Label(Widget):
|
||||
"""A text label to attach to something else, such as a chart axis.
|
||||
|
||||
This allows you to specify an offset, angle and many anchor
|
||||
properties relative to the label's origin. It allows, for example,
|
||||
angled multiline axis labels.
|
||||
"""
|
||||
# fairly straight port of Robin Becker's textbox.py to new widgets
|
||||
# framework.
|
||||
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber,desc=''),
|
||||
y = AttrMapValue(isNumber,desc=''),
|
||||
dx = AttrMapValue(isNumber,desc='delta x - offset'),
|
||||
dy = AttrMapValue(isNumber,desc='delta y - offset'),
|
||||
angle = AttrMapValue(isNumber,desc='angle of label: default (0), 90 is vertical, 180 is upside down, etc'),
|
||||
boxAnchor = AttrMapValue(isBoxAnchor,desc='anchoring point of the label'),
|
||||
boxStrokeColor = AttrMapValue(isColorOrNone,desc='border color of the box'),
|
||||
boxStrokeWidth = AttrMapValue(isNumber,desc='border width'),
|
||||
boxFillColor = AttrMapValue(isColorOrNone,desc='the filling color of the box'),
|
||||
boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi'),desc="one of ('normal','anti','lo','hi')"),
|
||||
boxRx = AttrMapValue(isNumber,desc='box corner x radius'),
|
||||
boxRy = AttrMapValue(isNumber,desc='box corner y radius'),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc='label text color'),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc='label text border color'),
|
||||
strokeWidth = AttrMapValue(isNumber,desc='label text border width'),
|
||||
text = AttrMapValue(isString,desc='the actual text to display'),
|
||||
fontName = AttrMapValue(isString,desc='the name of the font used'),
|
||||
fontSize = AttrMapValue(isNumber,desc='the size of the font'),
|
||||
leading = AttrMapValue(isNumberOrNone,desc=''),
|
||||
width = AttrMapValue(isNumberOrNone,desc='the width of the label'),
|
||||
maxWidth = AttrMapValue(isNumberOrNone,desc='maximum width the label can grow to'),
|
||||
height = AttrMapValue(isNumberOrNone,desc='the height of the text'),
|
||||
textAnchor = AttrMapValue(isTextAnchor,desc='the anchoring point of the text inside the label'),
|
||||
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
|
||||
topPadding = AttrMapValue(isNumber,desc='padding at top of box'),
|
||||
leftPadding = AttrMapValue(isNumber,desc='padding at left of box'),
|
||||
rightPadding = AttrMapValue(isNumber,desc='padding at right of box'),
|
||||
bottomPadding = AttrMapValue(isNumber,desc='padding at bottom of box'),
|
||||
padding = AttrMapValue(EitherOr((isNumberOrNone,isListOfNumbers)),'TRBL css like padding'),
|
||||
useAscentDescent = AttrMapValue(isBoolean,desc="If True then the font's Ascent & Descent will be used to compute default heights and baseline."),
|
||||
customDrawChanger = AttrMapValue(isNoneOrCallable,desc="An instance of CustomDrawChanger to modify the behavior at draw time", _advancedUsage=1),
|
||||
ddf = AttrMapValue(NoneOr(isSubclassOf(DirectDraw),'NoneOrDirectDraw'),desc="A DirectDrawFlowable instance", _advancedUsage=1),
|
||||
ddfKlass = AttrMapValue(NoneOr(isSubclassOf(Flowable),'NoneOrDirectDraw'),desc="A Flowable class for direct drawing (default is XPreformatted", _advancedUsage=1),
|
||||
ddfStyle = AttrMapValue(NoneOr((isSubclassOf(PropertySet),isInstanceOf(PropertySet))),desc="A style or style class for a ddfKlass or None", _advancedUsage=1),
|
||||
)
|
||||
|
||||
def __init__(self,**kw):
|
||||
self._setKeywords(**kw)
|
||||
self._setKeywords(
|
||||
_text = 'Multi-Line\nString',
|
||||
boxAnchor = 'c',
|
||||
angle = 0,
|
||||
x = 0,
|
||||
y = 0,
|
||||
dx = 0,
|
||||
dy = 0,
|
||||
topPadding = 0,
|
||||
leftPadding = 0,
|
||||
rightPadding = 0,
|
||||
bottomPadding = 0,
|
||||
boxStrokeWidth = 0.5,
|
||||
boxStrokeColor = None,
|
||||
boxTarget = 'normal',
|
||||
boxRx = 0,
|
||||
boxRy = 0,
|
||||
strokeColor = None,
|
||||
boxFillColor = None,
|
||||
leading = None,
|
||||
width = None,
|
||||
maxWidth = None,
|
||||
height = None,
|
||||
fillColor = STATE_DEFAULTS['fillColor'],
|
||||
fontName = STATE_DEFAULTS['fontName'],
|
||||
fontSize = STATE_DEFAULTS['fontSize'],
|
||||
strokeWidth = 0.1,
|
||||
textAnchor = 'start',
|
||||
visible = 1,
|
||||
useAscentDescent = False,
|
||||
ddf = DirectDrawFlowable,
|
||||
ddfKlass = getattr(self.__class__,'ddfKlass',None),
|
||||
ddfStyle = getattr(self.__class__,'ddfStyle',None),
|
||||
)
|
||||
|
||||
@property
|
||||
def padding(self):
|
||||
p = self.topPadding, self.rightPadding, self.bottomPadding, self.leftPadding
|
||||
n = len(set(p))
|
||||
if n==1: return p[0]
|
||||
elif n==2 and p[0]==p[2] and p[1]==p[3]: return p[:2]
|
||||
elif n==3 and p[1]==p[3]: return p[:3]
|
||||
return p
|
||||
|
||||
@padding.setter
|
||||
def padding(self,p):
|
||||
self.topPadding, self.rightPadding, self.bottomPadding, self.leftPadding = normalizeTRBL(p)
|
||||
|
||||
def setText(self, text):
|
||||
"""Set the text property. May contain embedded newline characters.
|
||||
Called by the containing chart or axis."""
|
||||
self._text = text
|
||||
|
||||
|
||||
def setOrigin(self, x, y):
|
||||
"""Set the origin. This would be the tick mark or bar top relative to
|
||||
which it is defined. Called by the containing chart or axis."""
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
|
||||
def demo(self):
|
||||
"""This shows a label positioned with its top right corner
|
||||
at the top centre of the drawing, and rotated 45 degrees."""
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
# mark the origin of the label
|
||||
d.add(Circle(100,90, 5, fillColor=colors.green))
|
||||
|
||||
lab = Label()
|
||||
lab.setOrigin(100,90)
|
||||
lab.boxAnchor = 'ne'
|
||||
lab.angle = 45
|
||||
lab.dx = 0
|
||||
lab.dy = -20
|
||||
lab.boxStrokeColor = colors.green
|
||||
lab.setText('Another\nMulti-Line\nString')
|
||||
d.add(lab)
|
||||
|
||||
return d
|
||||
|
||||
def _getBoxAnchor(self):
|
||||
'''hook for allowing special box anchor effects'''
|
||||
ba = self.boxAnchor
|
||||
if ba in ('autox', 'autoy'):
|
||||
angle = self.angle
|
||||
na = (int((angle%360)/45.)*45)%360
|
||||
if not (na % 90): # we have a right angle case
|
||||
da = (angle - na) % 360
|
||||
if abs(da)>5:
|
||||
na = na + (da>0 and 45 or -45)
|
||||
ba = _A2BA[ba[-1]][na]
|
||||
return ba
|
||||
|
||||
def _getBaseLineRatio(self):
|
||||
if self.useAscentDescent:
|
||||
self._ascent, self._descent = getAscentDescent(self.fontName,self.fontSize)
|
||||
self._baselineRatio = self._ascent/(self._ascent-self._descent)
|
||||
else:
|
||||
self._baselineRatio = 1/1.2
|
||||
|
||||
def _computeSizeEnd(self,objH):
|
||||
self._height = self.height or (objH + self.topPadding + self.bottomPadding)
|
||||
self._ewidth = (self._width-self.leftPadding-self.rightPadding)
|
||||
self._eheight = (self._height-self.topPadding-self.bottomPadding)
|
||||
boxAnchor = self._getBoxAnchor()
|
||||
if boxAnchor in ['n','ne','nw']:
|
||||
self._top = -self.topPadding
|
||||
elif boxAnchor in ['s','sw','se']:
|
||||
self._top = self._height-self.topPadding
|
||||
else:
|
||||
self._top = 0.5*self._eheight
|
||||
self._bottom = self._top - self._eheight
|
||||
|
||||
if boxAnchor in ['ne','e','se']:
|
||||
self._left = self.leftPadding - self._width
|
||||
elif boxAnchor in ['nw','w','sw']:
|
||||
self._left = self.leftPadding
|
||||
else:
|
||||
self._left = -self._ewidth*0.5
|
||||
self._right = self._left+self._ewidth
|
||||
|
||||
def computeSize(self):
|
||||
# the thing will draw in its own coordinate system
|
||||
ddfKlass = getattr(self,'ddfKlass',None)
|
||||
if not ddfKlass:
|
||||
self._lineWidths = []
|
||||
self._lines = simpleSplit(self._text,self.fontName,self.fontSize,self.maxWidth)
|
||||
if not self.width:
|
||||
self._width = self.leftPadding+self.rightPadding
|
||||
if self._lines:
|
||||
self._lineWidths = [stringWidth(line,self.fontName,self.fontSize) for line in self._lines]
|
||||
self._width += max(self._lineWidths)
|
||||
else:
|
||||
self._width = self.width
|
||||
self._getBaseLineRatio()
|
||||
if self.leading:
|
||||
self._leading = self.leading
|
||||
elif self.useAscentDescent:
|
||||
self._leading = self._ascent - self._descent
|
||||
else:
|
||||
self._leading = self.fontSize*1.2
|
||||
objH = self._leading*len(self._lines)
|
||||
else:
|
||||
if self.ddf is None:
|
||||
raise RuntimeError('DirectDrawFlowable class is not available you need the rlextra package as well as reportlab')
|
||||
sty = dict(
|
||||
name='xlabel-generated',
|
||||
fontName=self.fontName,
|
||||
fontSize=self.fontSize,
|
||||
fillColor=self.fillColor,
|
||||
strokeColor=self.strokeColor,
|
||||
)
|
||||
|
||||
if not self.ddfStyle:
|
||||
sty = ParagraphStyle(**sty)
|
||||
elif isinstance(self.ddfStyle,PropertySet):
|
||||
sty = self.ddfStyle.clone(**sty)
|
||||
elif isinstance(self.ddfStyle,type) and issubclass(self.ddfStyle,PropertySet):
|
||||
sty = self.ddfStyle(**sty)
|
||||
else:
|
||||
raise ValueError(f'ddfStyle has invalid type {type(self.ddfStyle)}')
|
||||
|
||||
self._style = sty
|
||||
self._getBaseLineRatio()
|
||||
if self.useAscentDescent:
|
||||
sty.autoLeading = True
|
||||
sty.leading = self._ascent - self._descent
|
||||
else:
|
||||
sty.leading = self.leading if self.leading else self.fontSize*1.2
|
||||
self._leading = sty.leading
|
||||
ta = self._getTextAnchor()
|
||||
|
||||
aW = self.maxWidth or 0x7fffffff
|
||||
if ta!='start':
|
||||
sty.alignment = TA_LEFT
|
||||
obj = ddfKlass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
aW = self.maxWidth or obj._width_max
|
||||
sty.alignment = _ta2al[ta]
|
||||
self._ddfObj = obj = ddfKlass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
|
||||
if not self.width:
|
||||
self._width = self.leftPadding+self.rightPadding
|
||||
self._width += obj._width_max
|
||||
else:
|
||||
self._width = self.width
|
||||
self._computeSizeEnd(objH)
|
||||
|
||||
def _getTextAnchor(self):
|
||||
'''This can be overridden to allow special effects'''
|
||||
ta = self.textAnchor
|
||||
if ta=='boxauto': ta = _BA2TA[self._getBoxAnchor()]
|
||||
return ta
|
||||
|
||||
def _rawDraw(self):
|
||||
_text = self._text
|
||||
self._text = _text or ''
|
||||
self.computeSize()
|
||||
self._text = _text
|
||||
g = Group()
|
||||
g.translate(self.x + self.dx, self.y + self.dy)
|
||||
g.rotate(self.angle)
|
||||
|
||||
ddfKlass = getattr(self,'ddfKlass',None)
|
||||
if ddfKlass:
|
||||
x = self._left
|
||||
else:
|
||||
y = self._top - self._leading*self._baselineRatio
|
||||
textAnchor = self._getTextAnchor()
|
||||
if textAnchor == 'start':
|
||||
x = self._left
|
||||
elif textAnchor == 'middle':
|
||||
x = self._left + self._ewidth*0.5
|
||||
else:
|
||||
x = self._right
|
||||
|
||||
# paint box behind text just in case they
|
||||
# fill it
|
||||
if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth):
|
||||
g.add(Rect( self._left-self.leftPadding,
|
||||
self._bottom-self.bottomPadding,
|
||||
self._width,
|
||||
self._height,
|
||||
strokeColor=self.boxStrokeColor,
|
||||
strokeWidth=self.boxStrokeWidth,
|
||||
fillColor=self.boxFillColor,
|
||||
rx=self.boxRx,
|
||||
ry=self.boxRy,
|
||||
))
|
||||
|
||||
if ddfKlass:
|
||||
g1 = Group()
|
||||
g1.translate(x,self._top-self._eheight)
|
||||
g1.add(self.ddf(self._ddfObj))
|
||||
g.add(g1)
|
||||
else:
|
||||
fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize
|
||||
strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, self._leading
|
||||
svgAttrs=getattr(self,'_svgAttrs',{})
|
||||
if strokeColor:
|
||||
for line in self._lines:
|
||||
s = _text2Path(line, x, y, fontName, fontSize, textAnchor)
|
||||
s.fillColor = fillColor
|
||||
s.strokeColor = strokeColor
|
||||
s.strokeWidth = strokeWidth
|
||||
g.add(s)
|
||||
y -= leading
|
||||
else:
|
||||
for line in self._lines:
|
||||
s = String(x, y, line, _svgAttrs=svgAttrs)
|
||||
s.textAnchor = textAnchor
|
||||
s.fontName = fontName
|
||||
s.fontSize = fontSize
|
||||
s.fillColor = fillColor
|
||||
g.add(s)
|
||||
y -= leading
|
||||
|
||||
return g
|
||||
|
||||
def draw(self):
|
||||
customDrawChanger = getattr(self,'customDrawChanger',None)
|
||||
if customDrawChanger:
|
||||
customDrawChanger(True,self)
|
||||
try:
|
||||
return self._rawDraw()
|
||||
finally:
|
||||
customDrawChanger(False,self)
|
||||
else:
|
||||
return self._rawDraw()
|
||||
|
||||
class LabelDecorator:
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumberOrNone,desc=''),
|
||||
y = AttrMapValue(isNumberOrNone,desc=''),
|
||||
dx = AttrMapValue(isNumberOrNone,desc=''),
|
||||
dy = AttrMapValue(isNumberOrNone,desc=''),
|
||||
angle = AttrMapValue(isNumberOrNone,desc=''),
|
||||
boxAnchor = AttrMapValue(isBoxAnchor,desc=''),
|
||||
boxStrokeColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
boxStrokeWidth = AttrMapValue(isNumberOrNone,desc=''),
|
||||
boxFillColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
strokeWidth = AttrMapValue(isNumberOrNone),desc='',
|
||||
fontName = AttrMapValue(isNoneOrString,desc=''),
|
||||
fontSize = AttrMapValue(isNumberOrNone,desc=''),
|
||||
leading = AttrMapValue(isNumberOrNone,desc=''),
|
||||
width = AttrMapValue(isNumberOrNone,desc=''),
|
||||
maxWidth = AttrMapValue(isNumberOrNone,desc=''),
|
||||
height = AttrMapValue(isNumberOrNone,desc=''),
|
||||
textAnchor = AttrMapValue(isTextAnchor,desc=''),
|
||||
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.textAnchor = 'start'
|
||||
self.boxAnchor = 'w'
|
||||
for a in self._attrMap.keys():
|
||||
if not hasattr(self,a): setattr(self,a,None)
|
||||
|
||||
def decorate(self,l,L):
|
||||
chart,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0 = l._callOutInfo
|
||||
L.setText(chart.categoryAxis.categoryNames[colNo])
|
||||
g.add(L)
|
||||
|
||||
def __call__(self,l):
|
||||
L = Label()
|
||||
for a,v in self.__dict__.items():
|
||||
if v is None: v = getattr(l,a,None)
|
||||
setattr(L,a,v)
|
||||
self.decorate(l,L)
|
||||
|
||||
isOffsetMode=OneOf('high','low','bar','axis')
|
||||
class LabelOffset(PropHolder):
|
||||
_attrMap = AttrMap(
|
||||
posMode = AttrMapValue(isOffsetMode,desc="Where to base +ve offset"),
|
||||
pos = AttrMapValue(isNumber,desc='Value for positive elements'),
|
||||
negMode = AttrMapValue(isOffsetMode,desc="Where to base -ve offset"),
|
||||
neg = AttrMapValue(isNumber,desc='Value for negative elements'),
|
||||
)
|
||||
def __init__(self):
|
||||
self.posMode=self.negMode='axis'
|
||||
self.pos = self.neg = 0
|
||||
|
||||
def _getValue(self, chart, val):
|
||||
flipXY = chart._flipXY
|
||||
A = chart.categoryAxis
|
||||
jA = A.joinAxis
|
||||
if val>=0:
|
||||
mode = self.posMode
|
||||
delta = self.pos
|
||||
else:
|
||||
mode = self.negMode
|
||||
delta = self.neg
|
||||
if flipXY:
|
||||
v = A._x
|
||||
else:
|
||||
v = A._y
|
||||
if jA:
|
||||
if flipXY:
|
||||
_v = jA._x
|
||||
else:
|
||||
_v = jA._y
|
||||
if mode=='high':
|
||||
v = _v + jA._length
|
||||
elif mode=='low':
|
||||
v = _v
|
||||
elif mode=='bar':
|
||||
v = _v+val
|
||||
return v+delta
|
||||
|
||||
NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset))
|
||||
|
||||
class PMVLabel(Label):
|
||||
_attrMap = AttrMap(
|
||||
BASE=Label,
|
||||
)
|
||||
|
||||
def __init__(self, **kwds):
|
||||
Label.__init__(self, **kwds)
|
||||
self._pmv = 0
|
||||
|
||||
def _getBoxAnchor(self):
|
||||
a = Label._getBoxAnchor(self)
|
||||
if self._pmv<0: a = {'nw':'se','n':'s','ne':'sw','w':'e','c':'c','e':'w','sw':'ne','s':'n','se':'nw'}[a]
|
||||
return a
|
||||
|
||||
def _getTextAnchor(self):
|
||||
a = Label._getTextAnchor(self)
|
||||
if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a]
|
||||
return a
|
||||
|
||||
class BarChartLabel(PMVLabel):
|
||||
"""
|
||||
An extended Label allowing for nudging, lines visibility etc
|
||||
"""
|
||||
_attrMap = AttrMap(
|
||||
BASE=PMVLabel,
|
||||
lineStrokeWidth = AttrMapValue(isNumberOrNone, desc="Non-zero for a drawn line"),
|
||||
lineStrokeColor = AttrMapValue(isColorOrNone, desc="Color for a drawn line"),
|
||||
fixedEnd = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw ends +/-"),
|
||||
fixedStart = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw starts +/-"),
|
||||
nudge = AttrMapValue(isNumber, desc="Non-zero sign dependent nudge"),
|
||||
boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi','mid'),desc="one of ('normal','anti','lo','hi','mid')"),
|
||||
)
|
||||
|
||||
def __init__(self, **kwds):
|
||||
PMVLabel.__init__(self, **kwds)
|
||||
self.lineStrokeWidth = 0
|
||||
self.lineStrokeColor = None
|
||||
self.fixedStart = self.fixedEnd = None
|
||||
self.nudge = 0
|
||||
|
||||
class NA_Label(BarChartLabel):
|
||||
"""
|
||||
An extended Label allowing for nudging, lines visibility etc
|
||||
"""
|
||||
_attrMap = AttrMap(
|
||||
BASE=BarChartLabel,
|
||||
text = AttrMapValue(isNoneOrString, desc="Text to be used for N/A values"),
|
||||
)
|
||||
def __init__(self):
|
||||
BarChartLabel.__init__(self)
|
||||
self.text = 'n/a'
|
||||
NoneOrInstanceOfNA_Label=NoneOr(isInstanceOf(NA_Label))
|
||||
|
||||
from reportlab.graphics.charts.utils import CustomDrawChanger
|
||||
class RedNegativeChanger(CustomDrawChanger):
|
||||
def __init__(self,fillColor=colors.red):
|
||||
CustomDrawChanger.__init__(self)
|
||||
self.fillColor = fillColor
|
||||
def _changer(self,obj):
|
||||
R = {}
|
||||
if obj._text.startswith('-'):
|
||||
R['fillColor'] = obj.fillColor
|
||||
obj.fillColor = self.fillColor
|
||||
return R
|
||||
|
||||
class XLabel(Label):
|
||||
'''like label but uses XPreFormatted/Paragraph to draw the _text'''
|
||||
_attrMap = AttrMap(BASE=Label,
|
||||
)
|
||||
def __init__(self,*args,**kwds):
|
||||
Label.__init__(self,*args,**kwds)
|
||||
self.ddfKlass = kwds.pop('ddfKlass',XPreformatted)
|
||||
self.ddf = kwds.pop('directDrawClass',self.ddf)
|
||||
|
||||
if False:
|
||||
def __init__(self,*args,**kwds):
|
||||
self._flowableClass = kwds.pop('flowableClass',XPreformatted)
|
||||
ddf = kwds.pop('directDrawClass',DirectDrawFlowable)
|
||||
if ddf is None:
|
||||
raise RuntimeError('DirectDrawFlowable class is not available you need the rlextra package as well as reportlab')
|
||||
self._ddf = ddf
|
||||
Label.__init__(self,*args,**kwds)
|
||||
def computeSize(self):
|
||||
# the thing will draw in its own coordinate system
|
||||
self._lineWidths = []
|
||||
sty = self._style = ParagraphStyle('xlabel-generated',
|
||||
fontName=self.fontName,
|
||||
fontSize=self.fontSize,
|
||||
fillColor=self.fillColor,
|
||||
strokeColor=self.strokeColor,
|
||||
)
|
||||
self._getBaseLineRatio()
|
||||
if self.useAscentDescent:
|
||||
sty.autoLeading = True
|
||||
sty.leading = self._ascent - self._descent
|
||||
else:
|
||||
sty.leading = self.leading if self.leading else self.fontSize*1.2
|
||||
self._leading = sty.leading
|
||||
ta = self._getTextAnchor()
|
||||
aW = self.maxWidth or 0x7fffffff
|
||||
if ta!='start':
|
||||
sty.alignment = TA_LEFT
|
||||
obj = self._flowableClass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
aW = self.maxWidth or obj._width_max
|
||||
sty.alignment = _ta2al[ta]
|
||||
self._obj = obj = self._flowableClass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
|
||||
if not self.width:
|
||||
self._width = self.leftPadding+self.rightPadding
|
||||
self._width += self._obj._width_max
|
||||
else:
|
||||
self._width = self.width
|
||||
self._computeSizeEnd(objH)
|
||||
|
||||
def _rawDraw(self):
|
||||
_text = self._text
|
||||
self._text = _text or ''
|
||||
self.computeSize()
|
||||
self._text = _text
|
||||
g = Group()
|
||||
g.translate(self.x + self.dx, self.y + self.dy)
|
||||
g.rotate(self.angle)
|
||||
|
||||
x = self._left
|
||||
|
||||
# paint box behind text just in case they
|
||||
# fill it
|
||||
if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth):
|
||||
g.add(Rect( self._left-self.leftPadding,
|
||||
self._bottom-self.bottomPadding,
|
||||
self._width,
|
||||
self._height,
|
||||
strokeColor=self.boxStrokeColor,
|
||||
strokeWidth=self.boxStrokeWidth,
|
||||
fillColor=self.boxFillColor)
|
||||
)
|
||||
g1 = Group()
|
||||
g1.translate(x,self._top-self._eheight)
|
||||
g1.add(self._ddf(self._obj))
|
||||
g.add(g1)
|
||||
return g
|
||||
@@ -0,0 +1,483 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2026
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/utils.py
|
||||
__all__ = (
|
||||
'angle2corner',
|
||||
'angle2dir',
|
||||
'boxCornerCoords',
|
||||
'CustomDrawChanger',
|
||||
'DrawTimeCollector',
|
||||
'FillPairedData',
|
||||
'find_good_grid',
|
||||
'find_interval',
|
||||
'findNones',
|
||||
'lineSegmentIntersect',
|
||||
'makeCircularString',
|
||||
'maverage',
|
||||
'mkTimeTuple',
|
||||
'nextRoundNumber',
|
||||
'pairFixNones',
|
||||
'pairMaverage',
|
||||
'seconds2str',
|
||||
'str2seconds',
|
||||
'ticks',
|
||||
'xyDist',
|
||||
)
|
||||
|
||||
__version__='3.4.8'
|
||||
__doc__="Utilities used here and there."
|
||||
from time import mktime, gmtime, strftime
|
||||
from math import log10, pi, floor, sin, cos, hypot
|
||||
import weakref
|
||||
from reportlab.graphics.shapes import transformPoints, inverse, Ellipse, Group, String, numericXShift
|
||||
from reportlab.lib.utils import flatten
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
|
||||
### Dinu's stuff used in some line plots (likely to vansih).
|
||||
def mkTimeTuple(timeString):
|
||||
"Convert a 'dd/mm/yyyy' formatted string to a tuple for use in the time module."
|
||||
|
||||
L = [0] * 9
|
||||
dd, mm, yyyy = list(map(int, timeString.split('/')))
|
||||
L[:3] = [yyyy, mm, dd]
|
||||
|
||||
return tuple(L)
|
||||
|
||||
def str2seconds(timeString):
|
||||
"Convert a number of seconds since the epoch into a date string."
|
||||
|
||||
return mktime(mkTimeTuple(timeString))
|
||||
|
||||
def seconds2str(seconds):
|
||||
"Convert a date string into the number of seconds since the epoch."
|
||||
|
||||
return strftime('%Y-%m-%d', gmtime(seconds))
|
||||
|
||||
### Aaron's rounding function for making nice values on axes.
|
||||
def nextRoundNumber(x):
|
||||
"""Return the first 'nice round number' greater than or equal to x
|
||||
|
||||
Used in selecting apropriate tick mark intervals; we say we want
|
||||
an interval which places ticks at least 10 points apart, work out
|
||||
what that is in chart space, and ask for the nextRoundNumber().
|
||||
Tries the series 1,2,5,10,20,50,100.., going up or down as needed.
|
||||
"""
|
||||
|
||||
#guess to nearest order of magnitude
|
||||
if x in (0, 1):
|
||||
return x
|
||||
|
||||
if x < 0:
|
||||
return -1.0 * nextRoundNumber(-x)
|
||||
else:
|
||||
lg = int(log10(x))
|
||||
|
||||
if lg == 0:
|
||||
if x < 1:
|
||||
base = 0.1
|
||||
else:
|
||||
base = 1.0
|
||||
elif lg < 0:
|
||||
base = 10.0 ** (lg - 1)
|
||||
else:
|
||||
base = 10.0 ** lg # e.g. base(153) = 100
|
||||
# base will always be lower than x
|
||||
|
||||
if base >= x:
|
||||
return base * 1.0
|
||||
elif (base * 2) >= x:
|
||||
return base * 2.0
|
||||
elif (base * 5) >= x:
|
||||
return base * 5.0
|
||||
else:
|
||||
return base * 10.0
|
||||
|
||||
_intervals=(.1, .2, .25, .5)
|
||||
_j_max=len(_intervals)-1
|
||||
def find_interval(lo,hi,I=5):
|
||||
'determine tick parameters for range [lo, hi] using I intervals'
|
||||
|
||||
if lo >= hi:
|
||||
if lo==hi:
|
||||
if lo==0:
|
||||
lo = -.1
|
||||
hi = .1
|
||||
else:
|
||||
lo = 0.9*lo
|
||||
hi = 1.1*hi
|
||||
else:
|
||||
raise ValueError("lo>hi")
|
||||
x=(hi - lo)/float(I)
|
||||
b= (x>0 and (x<1 or x>10)) and 10**floor(log10(x)) or 1
|
||||
b = b
|
||||
while 1:
|
||||
a = x/b
|
||||
if a<=_intervals[-1]: break
|
||||
b = b*10
|
||||
|
||||
j = 0
|
||||
while a>_intervals[j]: j = j + 1
|
||||
|
||||
while 1:
|
||||
ss = _intervals[j]*b
|
||||
n = lo/ss
|
||||
l = int(n)-(n<0)
|
||||
n = ss*l
|
||||
x = ss*(l+I)
|
||||
a = I*ss
|
||||
if n>0:
|
||||
if a>=hi:
|
||||
n = 0.0
|
||||
x = a
|
||||
elif hi<0:
|
||||
a = -a
|
||||
if lo>a:
|
||||
n = a
|
||||
x = 0
|
||||
if hi<=x and n<=lo: break
|
||||
j = j + 1
|
||||
if j>_j_max:
|
||||
j = 0
|
||||
b = b*10
|
||||
return n, x, ss, lo - n + x - hi
|
||||
|
||||
def find_good_grid(lower,upper,n=(4,5,6,7,8,9), grid=None):
|
||||
if grid:
|
||||
t = divmod(lower,grid)[0] * grid
|
||||
hi, z = divmod(upper,grid)
|
||||
if z>1e-8: hi = hi+1
|
||||
hi = hi*grid
|
||||
else:
|
||||
try:
|
||||
n[0]
|
||||
except TypeError:
|
||||
n = range(max(1,n-2),max(n+3,2))
|
||||
|
||||
w = 1e308
|
||||
for i in n:
|
||||
z=find_interval(lower,upper,i)
|
||||
if z[3]<w:
|
||||
t, hi, grid = z[:3]
|
||||
w=z[3]
|
||||
return t, hi, grid
|
||||
|
||||
def ticks(lower, upper, n=(4,5,6,7,8,9), split=1, percent=0, grid=None, labelVOffset=0):
|
||||
'''
|
||||
return tick positions and labels for range lower<=x<=upper
|
||||
n=number of intervals to try (can be a list or sequence)
|
||||
split=1 return ticks then labels else (tick,label) pairs
|
||||
'''
|
||||
t, hi, grid = find_good_grid(lower, upper, n, grid)
|
||||
power = floor(log10(grid))
|
||||
if power==0: power = 1
|
||||
w = grid/10.**power
|
||||
w = int(w)!=w
|
||||
|
||||
if power > 3 or power < -3:
|
||||
format = '%+'+repr(w+7)+'.0e'
|
||||
else:
|
||||
if power >= 0:
|
||||
digits = int(power)+w
|
||||
format = '%' + repr(digits)+'.0f'
|
||||
else:
|
||||
digits = w-int(power)
|
||||
format = '%'+repr(digits+2)+'.'+repr(digits)+'f'
|
||||
|
||||
if percent: format=format+'%%'
|
||||
T = []
|
||||
n = int(float(hi-t)/grid+0.1)+1
|
||||
if split:
|
||||
labels = []
|
||||
for i in range(n):
|
||||
v = t+grid*i
|
||||
T.append(v)
|
||||
labels.append(format % (v+labelVOffset))
|
||||
return T, labels
|
||||
else:
|
||||
for i in range(n):
|
||||
v = t+grid*i
|
||||
T.append((v, format % (v+labelVOffset)))
|
||||
return T
|
||||
|
||||
def findNones(data):
|
||||
m = len(data)
|
||||
if None in data:
|
||||
b = 0
|
||||
while b<m and data[b] is None:
|
||||
b += 1
|
||||
if b==m: return data
|
||||
l = m-1
|
||||
while data[l] is None:
|
||||
l -= 1
|
||||
l+=1
|
||||
if b or l: data = data[b:l]
|
||||
I = [i for i in range(len(data)) if data[i] is None]
|
||||
for i in I:
|
||||
data[i] = 0.5*(data[i-1]+data[i+1])
|
||||
return b, l, data
|
||||
return 0,m,data
|
||||
|
||||
def pairFixNones(pairs):
|
||||
Y = [x[1] for x in pairs]
|
||||
b,l,nY = findNones(Y)
|
||||
m = len(Y)
|
||||
if b or l<m or nY!=Y:
|
||||
if b or l<m: pairs = pairs[b:l]
|
||||
pairs = [(x[0],y) for x,y in zip(pairs,nY)]
|
||||
return pairs
|
||||
|
||||
def maverage(data,n=6):
|
||||
data = (n-1)*[data[0]]+data
|
||||
data = [float(sum(data[i-n:i]))/n for i in range(n,len(data)+1)]
|
||||
return data
|
||||
|
||||
def pairMaverage(data,n=6):
|
||||
return [(x[0],s) for x,s in zip(data, maverage([x[1] for x in data],n))]
|
||||
|
||||
class DrawTimeCollector:
|
||||
'''
|
||||
generic mechanism for collecting information about nodes at the time they are about to be drawn
|
||||
'''
|
||||
def __init__(self,formats=['gif']):
|
||||
self._nodes = weakref.WeakKeyDictionary()
|
||||
self.clear()
|
||||
self._pmcanv = None
|
||||
self.formats = formats
|
||||
self.disabled = False
|
||||
|
||||
def clear(self):
|
||||
self._info = []
|
||||
self._info_append = self._info.append
|
||||
|
||||
def record(self,func,node,*args,**kwds):
|
||||
self._nodes[node] = (func,args,kwds)
|
||||
node.__dict__['_drawTimeCallback'] = self
|
||||
|
||||
def __call__(self,node,canvas,renderer):
|
||||
func = self._nodes.get(node,None)
|
||||
if func:
|
||||
func, args, kwds = func
|
||||
i = func(node,canvas,renderer, *args, **kwds)
|
||||
if i is not None: self._info_append(i)
|
||||
|
||||
@staticmethod
|
||||
def rectDrawTimeCallback(node,canvas,renderer,**kwds):
|
||||
A = getattr(canvas,'ctm',None)
|
||||
if not A: return
|
||||
x1 = node.x
|
||||
y1 = node.y
|
||||
x2 = x1 + node.width
|
||||
y2 = y1 + node.height
|
||||
|
||||
D = kwds.copy()
|
||||
D['rect']=DrawTimeCollector.transformAndFlatten(A,((x1,y1),(x2,y2)))
|
||||
return D
|
||||
|
||||
@staticmethod
|
||||
def transformAndFlatten(A,p):
|
||||
''' transform an flatten a list of points
|
||||
A transformation matrix
|
||||
p points [(x0,y0),....(xk,yk).....]
|
||||
'''
|
||||
if tuple(A)!=(1,0,0,1,0,0):
|
||||
iA = inverse(A)
|
||||
p = transformPoints(iA,p)
|
||||
return tuple(flatten(p))
|
||||
|
||||
@property
|
||||
def pmcanv(self):
|
||||
if not self._pmcanv:
|
||||
import renderPM
|
||||
self._pmcanv = renderPM.PMCanvas(1,1)
|
||||
return self._pmcanv
|
||||
|
||||
def wedgeDrawTimeCallback(self,node,canvas,renderer,**kwds):
|
||||
A = getattr(canvas,'ctm',None)
|
||||
if not A: return
|
||||
if isinstance(node,Ellipse):
|
||||
c = self.pmcanv
|
||||
c.ellipse(node.cx, node.cy, node.rx,node.ry)
|
||||
p = c.vpath
|
||||
p = [(x[1],x[2]) for x in p]
|
||||
else:
|
||||
p = node.asPolygon().points
|
||||
p = [(p[i],p[i+1]) for i in range(0,len(p),2)]
|
||||
|
||||
D = kwds.copy()
|
||||
D['poly'] = self.transformAndFlatten(A,p)
|
||||
return D
|
||||
|
||||
def save(self,fnroot):
|
||||
'''
|
||||
save the current information known to this collector
|
||||
fnroot is the root name of a resource to name the saved info
|
||||
override this to get the right semantics for your collector
|
||||
'''
|
||||
import pprint
|
||||
f=open(fnroot+'.default-collector.out','w')
|
||||
try:
|
||||
pprint.pprint(self._info,f)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def xyDist(xxx_todo_changeme, xxx_todo_changeme1 ):
|
||||
'''return distance between two points'''
|
||||
(x0,y0) = xxx_todo_changeme
|
||||
(x1,y1) = xxx_todo_changeme1
|
||||
return hypot((x1-x0),(y1-y0))
|
||||
|
||||
def lineSegmentIntersect(xxx_todo_changeme2, xxx_todo_changeme3, xxx_todo_changeme4, xxx_todo_changeme5
|
||||
):
|
||||
(x00,y00) = xxx_todo_changeme2
|
||||
(x01,y01) = xxx_todo_changeme3
|
||||
(x10,y10) = xxx_todo_changeme4
|
||||
(x11,y11) = xxx_todo_changeme5
|
||||
p = x00,y00
|
||||
r = x01-x00,y01-y00
|
||||
|
||||
|
||||
q = x10,y10
|
||||
s = x11-x10,y11-y10
|
||||
|
||||
rs = float(r[0]*s[1]-r[1]*s[0])
|
||||
qp = q[0]-p[0],q[1]-p[1]
|
||||
|
||||
qpr = qp[0]*r[1]-qp[1]*r[0]
|
||||
qps = qp[0]*s[1]-qp[1]*s[0]
|
||||
|
||||
if abs(rs)<1e-8:
|
||||
if abs(qpr)<1e-8: return 'collinear'
|
||||
return None
|
||||
|
||||
t = qps/rs
|
||||
u = qpr/rs
|
||||
|
||||
if 0<=t<=1 and 0<=u<=1:
|
||||
return p[0]+t*r[0], p[1]+t*r[1]
|
||||
|
||||
def makeCircularString(x, y, radius, angle, text, fontName, fontSize, inside=0, G=None,textAnchor='start'):
|
||||
'''make a group with circular text in it'''
|
||||
if not G: G = Group()
|
||||
|
||||
angle %= 360
|
||||
pi180 = pi/180
|
||||
phi = angle*pi180
|
||||
width = stringWidth(text, fontName, fontSize)
|
||||
sig = inside and -1 or 1
|
||||
hsig = sig*0.5
|
||||
sig90 = sig*90
|
||||
|
||||
if textAnchor!='start':
|
||||
if textAnchor=='middle':
|
||||
phi += sig*(0.5*width)/radius
|
||||
elif textAnchor=='end':
|
||||
phi += sig*float(width)/radius
|
||||
elif textAnchor=='numeric':
|
||||
phi += sig*float(numericXShift(textAnchor,text,width,fontName,fontSize,None))/radius
|
||||
|
||||
for letter in text:
|
||||
width = stringWidth(letter, fontName, fontSize)
|
||||
beta = float(width)/radius
|
||||
h = Group()
|
||||
h.add(String(0, 0, letter, fontName=fontName,fontSize=fontSize,textAnchor="start"))
|
||||
h.translate(x+cos(phi)*radius,y+sin(phi)*radius) #translate to radius and angle
|
||||
h.rotate((phi-hsig*beta)/pi180-sig90) # rotate as needed
|
||||
G.add(h) #add to main group
|
||||
phi -= sig*beta #increment
|
||||
|
||||
return G
|
||||
|
||||
class CustomDrawChanger:
|
||||
'''
|
||||
a class to simplify making changes at draw time
|
||||
'''
|
||||
def __init__(self):
|
||||
self.store = None
|
||||
|
||||
def __call__(self,change,obj):
|
||||
if change:
|
||||
self.store = self._changer(obj)
|
||||
assert isinstance(self.store,dict), '%s.changer should return a dict of changed attributes' % self.__class__.__name__
|
||||
elif self.store is not None:
|
||||
for a,v in self.store.items():
|
||||
setattr(obj,a,v)
|
||||
self.store = None
|
||||
|
||||
def _changer(self,obj):
|
||||
'''
|
||||
When implemented this method should return a dictionary of
|
||||
original attribute values so that a future self(False,obj)
|
||||
can restore them.
|
||||
'''
|
||||
raise RuntimeError('Abstract method _changer called')
|
||||
|
||||
class FillPairedData(list):
|
||||
def __init__(self,v,other=0):
|
||||
list.__init__(self,v)
|
||||
self.other = other
|
||||
|
||||
_arange2dirs = [
|
||||
(-1,22.5,'e'),
|
||||
(22.5,67.5,'ne'),
|
||||
(67.5,112.5,'n'),
|
||||
(112.5,157.5,'nw'),
|
||||
(157.5,202.5,'w'),
|
||||
(202.5,247.5,'sw'),
|
||||
(247.5,292.5,'s'),
|
||||
(292.5,337.5,'se'),
|
||||
(337.5,361,'e'),
|
||||
]
|
||||
def angle2dir(angle):
|
||||
'''converts mathematical angle to a compass point from a math angle where
|
||||
0 degrees lies along the x axis ie east==0 degrees
|
||||
|
||||
>>> [angle2dir(_) for _ in [0,360]+[__[0] for __ in _arange2dirs]+[__[1] for __ in _arange2dirs]]
|
||||
['e', 'e', 'e', 'e', 'ne', 'n', 'nw', 'w', 'sw', 's', 'se', 'e', 'ne', 'n', 'nw', 'w', 'sw', 's', 'se', 'e']
|
||||
'''
|
||||
a = angle % 360
|
||||
for lo, hi, d in _arange2dirs:
|
||||
if lo<a<=hi: return d
|
||||
return 'c'
|
||||
#I tested the bisect version below; it works, but is fractionally slower
|
||||
#import bisect
|
||||
#_elemk = lambda _: _[1]
|
||||
#return _arange2dirs[bisect.bisect_left(_arange2dirs,angle % 360,key=_elemk)][2]
|
||||
|
||||
_cornerNames=dict(e='w',ne='sw',n='s',nw='se',w='e',sw='ne',s='n',se='nw',c='c')
|
||||
def angle2corner(angle):
|
||||
'''converts a direction angle to a box corner name effectively the reverse direction
|
||||
>>> [angle2corner(_) for _ in [0,360]+[__[0] for __ in _arange2dirs]+[__[1] for __ in _arange2dirs]]
|
||||
['w', 'w', 'w', 'w', 'sw', 's', 'se', 'e', 'ne', 'n', 'nw', 'w', 'sw', 's', 'se', 'e', 'ne', 'n', 'nw', 'w']
|
||||
'''
|
||||
return _cornerNames[angle2dir(angle)]
|
||||
|
||||
def boxCornerCoords(bb, cn):
|
||||
'''return (x,y) for bounding box and corner name
|
||||
>>> bb=(1,0,0,1);[boxCornerCoords(bb,_) for _ in 'c n ne e se s sw w nw'.split()]
|
||||
[(0.5, 0.5), (0.5, 1), (1, 1), (1, 0.5), (1, 0), (0.5, 0), (0, 0), (0, 0.5), (0, 1)]
|
||||
>>> boxCornerCoords(bb,'z')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid box corner name 'z'
|
||||
'''
|
||||
if bb[0]>bb[2] or bb[1]>bb[3]:
|
||||
bb = (min(bb[0],bb[2]),min(bb[1],bb[3]),max(bb[0],bb[2]),max(bb[1],bb[3]))
|
||||
if cn not in ('n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c'):
|
||||
raise ValueError(f'invalid box corner name {cn!r}')
|
||||
if cn in ('c','s','n'):
|
||||
x = (bb[0]+bb[2])/2
|
||||
elif cn in ('ne','e','se'):
|
||||
x = bb[2]
|
||||
else:
|
||||
x = bb[0]
|
||||
if cn in ('e','c','w'):
|
||||
y = (bb[1]+bb[3])/2
|
||||
elif cn in ('nw','n','ne'):
|
||||
y = bb[3]
|
||||
else:
|
||||
y = bb[1]
|
||||
return x, y
|
||||
|
||||
if __name__=='__main__':
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
@@ -0,0 +1,230 @@
|
||||
from reportlab.graphics.shapes import Drawing, Polygon, Line
|
||||
|
||||
def _getShaded(col,shd=None,shading=0.1):
|
||||
if shd is None:
|
||||
from reportlab.lib.colors import Blacker
|
||||
if col: shd = Blacker(col,1-shading)
|
||||
return shd
|
||||
|
||||
def _getLit(col,shd=None,lighting=0.1):
|
||||
if shd is None:
|
||||
from reportlab.lib.colors import Whiter
|
||||
if col: shd = Whiter(col,1-lighting)
|
||||
return shd
|
||||
|
||||
|
||||
def _draw_3d_bar(G, x1, x2, y0, yhigh, xdepth, ydepth,
|
||||
fillColor=None, fillColorShaded=None,
|
||||
strokeColor=None, strokeWidth=1, shading=0.1):
|
||||
fillColorShaded = _getShaded(fillColor,None,shading)
|
||||
fillColorShadedTop = _getShaded(fillColor,None,shading/2.0)
|
||||
|
||||
def _add_3d_bar(x1, x2, y1, y2, xoff, yoff,
|
||||
G=G,strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor):
|
||||
G.add(Polygon((x1,y1, x1+xoff,y1+yoff, x2+xoff,y2+yoff, x2,y2),
|
||||
strokeWidth=strokeWidth, strokeColor=strokeColor, fillColor=fillColor,strokeLineJoin=1))
|
||||
|
||||
usd = max(y0, yhigh)
|
||||
if xdepth or ydepth:
|
||||
if y0!=yhigh: #non-zero height
|
||||
_add_3d_bar( x2, x2, y0, yhigh, xdepth, ydepth, fillColor=fillColorShaded) #side
|
||||
|
||||
_add_3d_bar(x1, x2, usd, usd, xdepth, ydepth, fillColor=fillColorShadedTop) #top
|
||||
|
||||
G.add(Polygon((x1,y0,x2,y0,x2,yhigh,x1,yhigh),
|
||||
strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor,strokeLineJoin=1)) #front
|
||||
|
||||
if xdepth or ydepth:
|
||||
G.add(Line( x1, usd, x2, usd, strokeWidth=strokeWidth, strokeColor=strokeColor or fillColorShaded))
|
||||
|
||||
class _YStrip:
|
||||
def __init__(self,y0,y1, slope, fillColor, fillColorShaded, shading=0.1):
|
||||
self.y0 = y0
|
||||
self.y1 = y1
|
||||
self.slope = slope
|
||||
self.fillColor = fillColor
|
||||
self.fillColorShaded = _getShaded(fillColor,fillColorShaded,shading)
|
||||
|
||||
def _ystrip_poly( x0, x1, y0, y1, xoff, yoff):
|
||||
return [x0,y0,x0+xoff,y0+yoff,x1+xoff,y1+yoff,x1,y1]
|
||||
|
||||
|
||||
def _make_3d_line_info( G, x0, x1, y0, y1, z0, z1,
|
||||
theta_x, theta_y,
|
||||
fillColor, fillColorShaded=None, tileWidth=1,
|
||||
strokeColor=None, strokeWidth=None, strokeDashArray=None,
|
||||
shading=0.1):
|
||||
zwidth = abs(z1-z0)
|
||||
xdepth = zwidth*theta_x
|
||||
ydepth = zwidth*theta_y
|
||||
depth_slope = xdepth==0 and 1e150 or -ydepth/float(xdepth)
|
||||
|
||||
x = float(x1-x0)
|
||||
slope = x==0 and 1e150 or (y1-y0)/x
|
||||
|
||||
c = slope>depth_slope and _getShaded(fillColor,fillColorShaded,shading) or fillColor
|
||||
zy0 = z0*theta_y
|
||||
zx0 = z0*theta_x
|
||||
|
||||
tileStrokeWidth = 0.6
|
||||
if tileWidth is None:
|
||||
D = [(x1,y1)]
|
||||
else:
|
||||
T = ((y1-y0)**2+(x1-x0)**2)**0.5
|
||||
tileStrokeWidth *= tileWidth
|
||||
if T<tileWidth:
|
||||
D = [(x1,y1)]
|
||||
else:
|
||||
n = int(T/float(tileWidth))+1
|
||||
dx = float(x1-x0)/n
|
||||
dy = float(y1-y0)/n
|
||||
D = []
|
||||
a = D.append
|
||||
for i in range(1,n):
|
||||
a((x0+dx*i,y0+dy*i))
|
||||
|
||||
a = G.add
|
||||
x_0 = x0+zx0
|
||||
y_0 = y0+zy0
|
||||
for x,y in D:
|
||||
x_1 = x+zx0
|
||||
y_1 = y+zy0
|
||||
P = Polygon(_ystrip_poly(x_0, x_1, y_0, y_1, xdepth, ydepth),
|
||||
fillColor = c, strokeColor=c, strokeWidth=tileStrokeWidth)
|
||||
a((0,z0,z1,x_0,y_0,P))
|
||||
x_0 = x_1
|
||||
y_0 = y_1
|
||||
|
||||
from math import pi
|
||||
_pi_2 = pi*0.5
|
||||
_2pi = 2*pi
|
||||
_180_pi=180./pi
|
||||
|
||||
def _2rad(angle):
|
||||
return angle/_180_pi
|
||||
|
||||
def mod_2pi(radians):
|
||||
radians = radians % _2pi
|
||||
if radians<-1e-6: radians += _2pi
|
||||
return radians
|
||||
|
||||
def _2deg(o):
|
||||
return o*_180_pi
|
||||
|
||||
def _360(a):
|
||||
a %= 360
|
||||
if a<-1e-6: a += 360
|
||||
return a
|
||||
|
||||
_ZERO = 1e-8
|
||||
_ONE = 1-_ZERO
|
||||
class _Segment:
|
||||
def __init__(self,s,i,data):
|
||||
S = data[s]
|
||||
x0 = S[i-1][0]
|
||||
y0 = S[i-1][1]
|
||||
x1 = S[i][0]
|
||||
y1 = S[i][1]
|
||||
if x1<x0:
|
||||
x0,y0,x1,y1 = x1,y1,x0,y0
|
||||
# (y-y0)*(x1-x0) = (y1-y0)*(x-x0)
|
||||
# (x1-x0)*y + (y0-y1)*x = y0*(x1-x0)+x0*(y0-y1)
|
||||
# a*y+b*x = c
|
||||
self.a = float(x1-x0)
|
||||
self.b = float(y1-y0)
|
||||
self.x0 = x0
|
||||
self.x1 = x1
|
||||
self.y0 = y0
|
||||
self.y1 = y1
|
||||
self.series = s
|
||||
self.i = i
|
||||
self.s = s
|
||||
|
||||
def __str__(self):
|
||||
return '[(%s,%s),(%s,%s)]' % (self.x0,self.y0,self.x1,self.y1)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def intersect(self,o,I):
|
||||
'''try to find an intersection with _Segment o
|
||||
'''
|
||||
x0 = self.x0
|
||||
ox0 = o.x0
|
||||
assert x0<=ox0
|
||||
if ox0>self.x1: return 1
|
||||
if o.s==self.s and o.i in (self.i-1,self.i+1): return
|
||||
a = self.a
|
||||
b = self.b
|
||||
oa = o.a
|
||||
ob = o.b
|
||||
det = ob*a - oa*b
|
||||
if -1e-8<det<1e-8: return
|
||||
dx = x0 - ox0
|
||||
dy = self.y0 - o.y0
|
||||
u = (oa*dy - ob*dx)/det
|
||||
ou = (a*dy - b*dx)/det
|
||||
if u<0 or u>1 or ou<0 or ou>1: return
|
||||
x = x0 + u*a
|
||||
y = self.y0 + u*b
|
||||
if _ZERO<u<_ONE:
|
||||
t = self.s,self.i,x,y
|
||||
if t not in I: I.append(t)
|
||||
if _ZERO<ou<_ONE:
|
||||
t = o.s,o.i,x,y
|
||||
if t not in I: I.append(t)
|
||||
|
||||
def _segKey(a):
|
||||
return (a.x0,a.x1,a.y0,a.y1,a.s,a.i)
|
||||
|
||||
def find_intersections(data,small=0):
|
||||
'''
|
||||
data is a sequence of series
|
||||
each series is a list of (x,y) coordinates
|
||||
where x & y are ints or floats
|
||||
|
||||
find_intersections returns a sequence of 4-tuples
|
||||
i, j, x, y
|
||||
|
||||
where i is a data index j is an insertion position for data[i]
|
||||
and x, y are coordinates of an intersection of series data[i]
|
||||
with some other series. If correctly implemented we get all such
|
||||
intersections. We don't count endpoint intersections and consider
|
||||
parallel lines as non intersecting (even when coincident).
|
||||
We ignore segments that have an estimated size less than small.
|
||||
'''
|
||||
|
||||
#find all line segments
|
||||
S = []
|
||||
a = S.append
|
||||
for s in range(len(data)):
|
||||
ds = data[s]
|
||||
if not ds: continue
|
||||
n = len(ds)
|
||||
if n==1: continue
|
||||
for i in range(1,n):
|
||||
seg = _Segment(s,i,data)
|
||||
if seg.a+abs(seg.b)>=small: a(seg)
|
||||
S.sort(key=_segKey)
|
||||
I = []
|
||||
n = len(S)
|
||||
for i in range(0,n-1):
|
||||
s = S[i]
|
||||
for j in range(i+1,n):
|
||||
if s.intersect(S[j],I)==1: break
|
||||
I.sort()
|
||||
return I
|
||||
|
||||
if __name__=='__main__':
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.lib.colors import lightgrey, pink
|
||||
D = Drawing(300,200)
|
||||
_draw_3d_bar(D, 10, 20, 10, 50, 5, 5, fillColor=lightgrey, strokeColor=pink)
|
||||
_draw_3d_bar(D, 30, 40, 10, 45, 5, 5, fillColor=lightgrey, strokeColor=pink)
|
||||
|
||||
D.save(formats=['pdf'],outDir='.',fnRoot='_draw_3d_bar')
|
||||
|
||||
print(find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(.2666666667,0.4),(0.1,0.4),(0.1,0.2),(0,0),(1,1)],[(0,1),(0.4,0.1),(1,0.1)]]))
|
||||
print(find_intersections([[(0.1, 0.2), (0.1, 0.4)], [(0, 1), (0.4, 0.1)]]))
|
||||
print(find_intersections([[(0.2, 0.4), (0.1, 0.4)], [(0.1, 0.8), (0.4, 0.1)]]))
|
||||
print(find_intersections([[(0,0),(1,1)],[(0.4,0.1),(1,0.1)]]))
|
||||
print(find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(0,0),(1,1)],[(0.1,0.8),(0.4,0.1),(1,0.1)]]))
|
||||
@@ -0,0 +1,400 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/renderPDF.py
|
||||
# renderPDF - draws Drawings onto a canvas
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""Render Drawing objects within others PDFs or standalone
|
||||
|
||||
Usage::
|
||||
|
||||
import renderpdf
|
||||
renderpdf.draw(drawing, canvas, x, y)
|
||||
|
||||
Execute the script to see some test drawings.
|
||||
changed
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from reportlab.graphics.shapes import *
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
from reportlab import rl_config
|
||||
from reportlab.graphics.renderbase import Renderer, getStateDelta, renderScaledDrawing, STATE_DEFAULTS
|
||||
|
||||
# the main entry point for users...
|
||||
def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_):
|
||||
"""As it says"""
|
||||
R = _PDFRenderer()
|
||||
R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary)
|
||||
|
||||
class _PDFRenderer(Renderer):
|
||||
"""This draws onto a PDF document. It needs to be a class
|
||||
rather than a function, as some PDF-specific state tracking is
|
||||
needed outside of the state info in the SVG model."""
|
||||
|
||||
def __init__(self):
|
||||
self._stroke = 0
|
||||
self._fill = 0
|
||||
|
||||
def drawNode(self, node):
|
||||
"""This is the recursive method called for each node
|
||||
in the tree"""
|
||||
#print "pdf:drawNode", self
|
||||
#if node.__class__ is Wedge: stop
|
||||
if not (isinstance(node, Path) and node.isClipPath):
|
||||
self._canvas.saveState()
|
||||
|
||||
#apply state changes
|
||||
deltas = getStateDelta(node)
|
||||
self._tracker.push(deltas)
|
||||
self.applyStateChanges(deltas, {})
|
||||
|
||||
#draw the object, or recurse
|
||||
self.drawNodeDispatcher(node)
|
||||
|
||||
self._tracker.pop()
|
||||
if not (isinstance(node, Path) and node.isClipPath):
|
||||
self._canvas.restoreState()
|
||||
|
||||
def drawRect(self, rect):
|
||||
if rect.rx == rect.ry == 0:
|
||||
#plain old rectangle
|
||||
self._canvas.rect(
|
||||
rect.x, rect.y,
|
||||
rect.width, rect.height,
|
||||
stroke=self._stroke,
|
||||
fill=self._fill
|
||||
)
|
||||
else:
|
||||
#cheat and assume ry = rx; better to generalize
|
||||
#pdfgen roundRect function. TODO
|
||||
self._canvas.roundRect(
|
||||
rect.x, rect.y,
|
||||
rect.width, rect.height, rect.rx,
|
||||
fill=self._fill,
|
||||
stroke=self._stroke
|
||||
)
|
||||
|
||||
def drawImage(self, image):
|
||||
path = image.path
|
||||
# currently not implemented in other renderers
|
||||
if path and (hasattr(path,'mode') or os.path.exists(image.path)):
|
||||
self._canvas.drawInlineImage(
|
||||
path,
|
||||
image.x, image.y,
|
||||
image.width, image.height,
|
||||
)
|
||||
|
||||
def drawLine(self, line):
|
||||
if self._stroke:
|
||||
self._canvas.line(line.x1, line.y1, line.x2, line.y2)
|
||||
|
||||
def drawCircle(self, circle):
|
||||
self._canvas.circle(
|
||||
circle.cx, circle.cy, circle.r,
|
||||
fill=self._fill,
|
||||
stroke=self._stroke,
|
||||
)
|
||||
|
||||
def drawPolyLine(self, polyline):
|
||||
if self._stroke:
|
||||
assert len(polyline.points) >= 2, 'Polyline must have 2 or more points'
|
||||
head, tail = polyline.points[0:2], polyline.points[2:],
|
||||
path = self._canvas.beginPath()
|
||||
path.moveTo(head[0], head[1])
|
||||
for i in range(0, len(tail), 2):
|
||||
path.lineTo(tail[i], tail[i+1])
|
||||
self._canvas.drawPath(path)
|
||||
|
||||
def drawWedge(self, wedge):
|
||||
if wedge.annular:
|
||||
self.drawPath(wedge.asPolygon())
|
||||
else:
|
||||
centerx, centery, radius, startangledegrees, endangledegrees = \
|
||||
wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees
|
||||
yradius, radius1, yradius1 = wedge._xtraRadii()
|
||||
if yradius is None: yradius = radius
|
||||
angle = endangledegrees-startangledegrees
|
||||
path = self._canvas.beginPath()
|
||||
if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None):
|
||||
path.moveTo(centerx, centery)
|
||||
path.arcTo(centerx-radius, centery-yradius, centerx+radius, centery+yradius,
|
||||
startangledegrees, angle)
|
||||
else:
|
||||
path.arc(centerx-radius, centery-yradius, centerx+radius, centery+yradius,
|
||||
startangledegrees, angle)
|
||||
path.arcTo(centerx-radius1, centery-yradius1, centerx+radius1, centery+yradius1,
|
||||
endangledegrees, -angle)
|
||||
path.close()
|
||||
self._canvas.drawPath(path,
|
||||
fill=self._fill,
|
||||
stroke=self._stroke,
|
||||
)
|
||||
|
||||
def drawEllipse(self, ellipse):
|
||||
#need to convert to pdfgen's bounding box representation
|
||||
x1 = ellipse.cx - ellipse.rx
|
||||
x2 = ellipse.cx + ellipse.rx
|
||||
y1 = ellipse.cy - ellipse.ry
|
||||
y2 = ellipse.cy + ellipse.ry
|
||||
self._canvas.ellipse(x1,y1,x2,y2,fill=self._fill,stroke=self._stroke)
|
||||
|
||||
def drawPolygon(self, polygon):
|
||||
assert len(polygon.points) >= 2, 'Polyline must have 2 or more points'
|
||||
head, tail = polygon.points[0:2], polygon.points[2:],
|
||||
path = self._canvas.beginPath()
|
||||
path.moveTo(head[0], head[1])
|
||||
for i in range(0, len(tail), 2):
|
||||
path.lineTo(tail[i], tail[i+1])
|
||||
path.close()
|
||||
self._canvas.drawPath(
|
||||
path,
|
||||
stroke=self._stroke,
|
||||
fill=self._fill,
|
||||
)
|
||||
|
||||
def drawString(self, stringObj):
|
||||
textRenderMode = getattr(stringObj,'textRenderMode',0)
|
||||
needFill = textRenderMode in (0,2,4,6)
|
||||
needStroke = textRenderMode in (1,2,5,6)
|
||||
|
||||
if (self._fill and needFill) or (self._stroke and needStroke):
|
||||
S = self._tracker.getState()
|
||||
text_anchor, x, y, text, enc = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text, stringObj.encoding
|
||||
if not text_anchor in ['start','inherited']:
|
||||
font, font_size = S['fontName'], S['fontSize']
|
||||
textLen = stringWidth(text, font, font_size, enc)
|
||||
if text_anchor=='end':
|
||||
x -= textLen
|
||||
elif text_anchor=='middle':
|
||||
x -= textLen*0.5
|
||||
elif text_anchor=='numeric':
|
||||
x -= numericXShift(text_anchor,text,textLen,font,font_size,enc)
|
||||
else:
|
||||
raise ValueError('bad value for textAnchor '+str(text_anchor))
|
||||
self._canvas.drawString(x, y, text, mode=textRenderMode or None)
|
||||
|
||||
def drawPath(self, path):
|
||||
from reportlab.graphics.shapes import _renderPath
|
||||
pdfPath = self._canvas.beginPath()
|
||||
drawFuncs = (pdfPath.moveTo, pdfPath.lineTo, pdfPath.curveTo, pdfPath.close)
|
||||
autoclose = getattr(path,'autoclose','')
|
||||
fill = self._fill
|
||||
stroke = self._stroke
|
||||
isClosed = _renderPath(path, drawFuncs, forceClose=fill and autoclose=='pdf')
|
||||
dP = self._canvas.drawPath
|
||||
cP = self._canvas.clipPath if path.isClipPath else dP
|
||||
fillMode = getattr(path,'fillMode',None)
|
||||
if autoclose=='svg':
|
||||
if fill and stroke and not isClosed:
|
||||
cP(pdfPath, fill=fill, stroke=0)
|
||||
dP(pdfPath, stroke=stroke, fill=0, fillMode=fillMode)
|
||||
else:
|
||||
cP(pdfPath, fill=fill, stroke=stroke, fillMode=fillMode)
|
||||
elif autoclose=='pdf':
|
||||
cP(pdfPath, fill=fill, stroke=stroke, fillMode=fillMode)
|
||||
else:
|
||||
#our old broken default
|
||||
if not isClosed:
|
||||
fill = 0
|
||||
cP(pdfPath, fill=fill, stroke=stroke, fillMode=fillMode)
|
||||
|
||||
def setStrokeColor(self,c):
|
||||
self._canvas.setStrokeColor(c)
|
||||
|
||||
def setFillColor(self,c):
|
||||
self._canvas.setFillColor(c)
|
||||
|
||||
def applyStateChanges(self, delta, newState):
|
||||
"""This takes a set of states, and outputs the PDF operators
|
||||
needed to set those properties"""
|
||||
for key, value in (sorted(delta.items()) if rl_config.invariant else delta.items()):
|
||||
if key == 'transform':
|
||||
self._canvas.transform(value[0], value[1], value[2],
|
||||
value[3], value[4], value[5])
|
||||
elif key == 'strokeColor':
|
||||
#this has different semantics in PDF to SVG;
|
||||
#we always have a color, and either do or do
|
||||
#not apply it; in SVG one can have a 'None' color
|
||||
if value is None:
|
||||
self._stroke = 0
|
||||
else:
|
||||
self._stroke = 1
|
||||
self.setStrokeColor(value)
|
||||
elif key == 'strokeWidth':
|
||||
self._canvas.setLineWidth(value)
|
||||
elif key == 'strokeLineCap': #0,1,2
|
||||
self._canvas.setLineCap(value)
|
||||
elif key == 'strokeLineJoin':
|
||||
self._canvas.setLineJoin(value)
|
||||
# elif key == 'stroke_dasharray':
|
||||
# self._canvas.setDash(array=value)
|
||||
elif key == 'strokeDashArray':
|
||||
if value:
|
||||
if isinstance(value,(list,tuple)) and len(value)==2 and isinstance(value[1],(tuple,list)):
|
||||
phase = value[0]
|
||||
value = value[1]
|
||||
else:
|
||||
phase = 0
|
||||
self._canvas.setDash(value,phase)
|
||||
else:
|
||||
self._canvas.setDash()
|
||||
elif key == 'fillColor':
|
||||
#this has different semantics in PDF to SVG;
|
||||
#we always have a color, and either do or do
|
||||
#not apply it; in SVG one can have a 'None' color
|
||||
if value is None:
|
||||
self._fill = 0
|
||||
else:
|
||||
self._fill = 1
|
||||
self.setFillColor(value)
|
||||
elif key in ['fontSize', 'fontName']:
|
||||
# both need setting together in PDF
|
||||
# one or both might be in the deltas,
|
||||
# so need to get whichever is missing
|
||||
fontname = delta.get('fontName', self._canvas._fontname)
|
||||
fontsize = delta.get('fontSize', self._canvas._fontsize)
|
||||
self._canvas.setFont(fontname, fontsize)
|
||||
elif key=='fillOpacity':
|
||||
if value is not None:
|
||||
self._canvas.setFillAlpha(value)
|
||||
elif key=='strokeOpacity':
|
||||
if value is not None:
|
||||
self._canvas.setStrokeAlpha(value)
|
||||
elif key=='fillOverprint':
|
||||
self._canvas.setFillOverprint(value)
|
||||
elif key=='strokeOverprint':
|
||||
self._canvas.setStrokeOverprint(value)
|
||||
elif key=='overprintMask':
|
||||
self._canvas.setOverprintMask(value)
|
||||
elif key=='fillMode':
|
||||
self._canvas._fillMode = value
|
||||
|
||||
from reportlab.platypus import Flowable
|
||||
class GraphicsFlowable(Flowable):
|
||||
"""Flowable wrapper around a Pingo drawing"""
|
||||
def __init__(self, drawing):
|
||||
self.drawing = drawing
|
||||
self.width = self.drawing.width
|
||||
self.height = self.drawing.height
|
||||
|
||||
def draw(self):
|
||||
draw(self.drawing, self.canv, 0, 0)
|
||||
|
||||
def drawToFile(d, fn, msg="", showBoundary=rl_config._unset_, autoSize=1, **kwds):
|
||||
"""Makes a one-page PDF with just the drawing.
|
||||
|
||||
If autoSize=1, the PDF will be the same size as
|
||||
the drawing; if 0, it will place the drawing on
|
||||
an A4 page with a title above it - possibly overflowing
|
||||
if too big."""
|
||||
d = renderScaledDrawing(d)
|
||||
for x in ('Name','Size'):
|
||||
a = 'initialFont'+x
|
||||
kwds[a] = getattr(d,a,kwds.pop(a,STATE_DEFAULTS['font'+x]))
|
||||
metadataPath = kwds.pop('metadataPath',None)
|
||||
c = Canvas(fn,**kwds)
|
||||
if msg:
|
||||
c.setFont(rl_config.defaultGraphicsFontName, 36)
|
||||
c.drawString(80, 750, msg)
|
||||
c.setTitle(msg)
|
||||
|
||||
if autoSize:
|
||||
c.setPageSize((d.width, d.height))
|
||||
draw(d, c, 0, 0, showBoundary=showBoundary)
|
||||
else:
|
||||
#show with a title
|
||||
c.setFont(rl_config.defaultGraphicsFontName, 12)
|
||||
y = 740
|
||||
i = 1
|
||||
y = y - d.height
|
||||
draw(d, c, 80, y, showBoundary=showBoundary)
|
||||
|
||||
if metadataPath:
|
||||
from reportlab.pdfbase.pdfdoc import XMP
|
||||
c._doc.Catalog.Metadata = XMP(path=metadataPath)
|
||||
c.showPage()
|
||||
c.save()
|
||||
if sys.platform=='mac' and not hasattr(fn, "write"):
|
||||
try:
|
||||
import macfs, macostools
|
||||
macfs.FSSpec(fn).SetCreatorType("CARO", "PDF ")
|
||||
macostools.touched(fn)
|
||||
except:
|
||||
pass
|
||||
|
||||
def drawToString(d, msg="", showBoundary=rl_config._unset_,autoSize=1,**kwds):
|
||||
"Returns a PDF as a string in memory, without touching the disk"
|
||||
s = BytesIO()
|
||||
drawToFile(d, s, msg=msg, showBoundary=showBoundary,autoSize=autoSize, **kwds)
|
||||
return s.getvalue()
|
||||
|
||||
#########################################################
|
||||
#
|
||||
# test code. First, define a bunch of drawings.
|
||||
# Routine to draw them comes at the end.
|
||||
#
|
||||
#########################################################
|
||||
def test(outDir='pdfout',shout=False):
|
||||
from reportlab.graphics.shapes import _baseGFontName, _baseGFontNameBI
|
||||
from reportlab.rl_config import verbose
|
||||
import os
|
||||
if not os.path.isdir(outDir):
|
||||
os.mkdir(outDir)
|
||||
fn = os.path.join(outDir,'renderPDF.pdf')
|
||||
c = Canvas(fn)
|
||||
c.setFont(_baseGFontName, 36)
|
||||
c.drawString(80, 750, 'Graphics Test')
|
||||
|
||||
# print all drawings and their doc strings from the test
|
||||
# file
|
||||
|
||||
#grab all drawings from the test module
|
||||
from reportlab.graphics import testshapes
|
||||
drawings = []
|
||||
for funcname in dir(testshapes):
|
||||
if funcname[0:10] == 'getDrawing':
|
||||
func = getattr(testshapes,funcname)
|
||||
drawing = func() #execute it
|
||||
docstring = getattr(func,'__doc__','')
|
||||
drawings.append((drawing, docstring))
|
||||
|
||||
#print in a loop, with their doc strings
|
||||
c.setFont(_baseGFontName, 12)
|
||||
y = 740
|
||||
i = 1
|
||||
for (drawing, docstring) in drawings:
|
||||
assert (docstring is not None), "Drawing %d has no docstring!" % i
|
||||
if y < 300: #allows 5-6 lines of text
|
||||
c.showPage()
|
||||
y = 740
|
||||
# draw a title
|
||||
y = y - 30
|
||||
c.setFont(_baseGFontNameBI,12)
|
||||
c.drawString(80, y, 'Drawing %d' % i)
|
||||
c.setFont(_baseGFontName,12)
|
||||
y = y - 14
|
||||
textObj = c.beginText(80, y)
|
||||
textObj.textLines(docstring)
|
||||
c.drawText(textObj)
|
||||
y = textObj.getY()
|
||||
y = y - drawing.height
|
||||
draw(drawing, c, 80, y)
|
||||
i = i + 1
|
||||
if y!=740: c.showPage()
|
||||
|
||||
c.save()
|
||||
if shout or verbose>2:
|
||||
print('saved %s' % ascii(fn))
|
||||
|
||||
if __name__=='__main__':
|
||||
test(shout=True)
|
||||
import sys
|
||||
if len(sys.argv)>1:
|
||||
outdir = sys.argv[1]
|
||||
else:
|
||||
outdir = 'pdfout'
|
||||
test(outdir,shout=True)
|
||||
#testFlowable()
|
||||
@@ -0,0 +1,824 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/graphics/Csrc/renderPM/renderP.py
|
||||
__version__='3.3.0'
|
||||
__doc__="""Render drawing objects in common bitmap formats
|
||||
|
||||
Usage::
|
||||
|
||||
from reportlab.graphics import renderPM
|
||||
renderPM.drawToFile(drawing,filename,fmt='GIF',configPIL={....})
|
||||
|
||||
Other functions let you create a PM drawing as string or into a PM buffer.
|
||||
Execute the script to see some test drawings."""
|
||||
|
||||
from reportlab.graphics.shapes import *
|
||||
from reportlab.graphics.renderbase import getStateDelta, renderScaledDrawing
|
||||
from reportlab.pdfbase.pdfmetrics import getFont, unicode2T1, stringWidth
|
||||
from reportlab.pdfbase.ttfonts import ShapedStr, shapeFragWord
|
||||
from reportlab.pdfgen.textobject import bidiShapedText
|
||||
from reportlab.lib.utils import isUnicode, asUnicode
|
||||
from reportlab.lib.abag import ABag
|
||||
from reportlab.lib.colors import toColor, white
|
||||
from reportlab import rl_config
|
||||
from .utils import setFont as _setFont, RenderPMError
|
||||
|
||||
import os, sys
|
||||
from io import BytesIO, StringIO
|
||||
from math import sin, cos, pi, ceil
|
||||
|
||||
def _getPMBackend(backend=None):
|
||||
if not backend: backend = rl_config.renderPMBackend
|
||||
if 'cairo' in backend.lower():
|
||||
try:
|
||||
import rlPyCairo as M
|
||||
except ImportError as errMsg:
|
||||
raise RenderPMError(f"""cannot import desired renderPM backend {backend}
|
||||
Seek advice at the users list see
|
||||
https://groups.google.com/g/reportlab-users""")
|
||||
else:
|
||||
raise RenderPMError(f'Invalid renderPM backend, {backend}')
|
||||
return M
|
||||
|
||||
try:
|
||||
_pmBackend = _getPMBackend(rl_config.renderPMBackend)
|
||||
except RenderPMError:
|
||||
_pmBackend=None
|
||||
|
||||
def _getImage():
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
import Image
|
||||
return Image
|
||||
|
||||
def Color2Hex(c):
|
||||
#assert isinstance(colorobj, colors.Color) #these checks don't work well RGB
|
||||
if c: return ((0xFF&int(255*c.red)) << 16) | ((0xFF&int(255*c.green)) << 8) | (0xFF&int(255*c.blue))
|
||||
return c
|
||||
|
||||
def CairoColor(c):
|
||||
'''
|
||||
c should be None or something convertible to Color
|
||||
rlPyCairo.GState can handle Color directly in either RGB24 or ARGB32
|
||||
'''
|
||||
return toColor(c) if c is not None else c
|
||||
|
||||
# the main entry point for users...
|
||||
def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_,**kwds):
|
||||
"""As it says"""
|
||||
R = _PMRenderer()
|
||||
R.__dict__.update(kwds)
|
||||
R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary)
|
||||
|
||||
from reportlab.graphics.renderbase import Renderer
|
||||
class _PMRenderer(Renderer):
|
||||
"""This draws onto a pix map image. It needs to be a class
|
||||
rather than a function, as some image-specific state tracking is
|
||||
needed outside of the state info in the SVG model."""
|
||||
|
||||
def pop(self):
|
||||
self._tracker.pop()
|
||||
self.applyState()
|
||||
|
||||
def push(self,node):
|
||||
deltas = getStateDelta(node)
|
||||
self._tracker.push(deltas)
|
||||
self.applyState()
|
||||
|
||||
def applyState(self):
|
||||
s = self._tracker.getState()
|
||||
self._canvas.ctm = s['ctm']
|
||||
self._canvas.strokeWidth = s['strokeWidth']
|
||||
alpha = s['strokeOpacity']
|
||||
if alpha is not None:
|
||||
self._canvas.strokeOpacity = alpha
|
||||
self._canvas.setStrokeColor(s['strokeColor'])
|
||||
self._canvas.lineCap = s['strokeLineCap']
|
||||
self._canvas.lineJoin = s['strokeLineJoin']
|
||||
self._canvas.fillMode = s['fillMode']
|
||||
da = s['strokeDashArray']
|
||||
if not da:
|
||||
da = None
|
||||
else:
|
||||
if not isinstance(da,(list,tuple)):
|
||||
da = da,
|
||||
if len(da)!=2 or not isinstance(da[1],(list,tuple)):
|
||||
da = 0, da #assume phase of 0
|
||||
self._canvas.dashArray = da
|
||||
alpha = s['fillOpacity']
|
||||
if alpha is not None:
|
||||
self._canvas.fillOpacity = alpha
|
||||
self._canvas.setFillColor(s['fillColor'])
|
||||
self._canvas.setFont(s['fontName'], s['fontSize'])
|
||||
|
||||
def initState(self,x,y):
|
||||
deltas = self._tracker._combined[-1]
|
||||
deltas['transform'] = deltas['ctm'] = self._canvas._baseCTM[0:4]+(x,y)
|
||||
self._tracker.push(deltas)
|
||||
self.applyState()
|
||||
|
||||
def drawNode(self, node):
|
||||
"""This is the recursive method called for each node
|
||||
in the tree"""
|
||||
|
||||
#apply state changes
|
||||
self.push(node)
|
||||
|
||||
#draw the object, or recurse
|
||||
self.drawNodeDispatcher(node)
|
||||
|
||||
# restore the state
|
||||
self.pop()
|
||||
|
||||
def drawRect(self, rect):
|
||||
c = self._canvas
|
||||
if rect.rx == rect.ry == 0:
|
||||
#plain old rectangle, draw clockwise (x-axis to y-axis) direction
|
||||
c.rect(rect.x,rect.y, rect.width, rect.height)
|
||||
else:
|
||||
c.roundRect(rect.x,rect.y, rect.width, rect.height, rect.rx, rect.ry)
|
||||
|
||||
def drawLine(self, line):
|
||||
self._canvas.line(line.x1,line.y1,line.x2,line.y2)
|
||||
|
||||
def drawImage(self, image):
|
||||
path = image.path
|
||||
if isinstance(path,str):
|
||||
if not (path and os.path.isfile(path)): return
|
||||
im = _getImage().open(path).convert('RGB')
|
||||
elif hasattr(path,'convert'):
|
||||
im = path.convert('RGB')
|
||||
else:
|
||||
return
|
||||
srcW, srcH = im.size
|
||||
dstW, dstH = image.width, image.height
|
||||
if dstW is None: dstW = srcW
|
||||
if dstH is None: dstH = srcH
|
||||
self._canvas._aapixbuf(
|
||||
image.x, image.y, dstW, dstH,
|
||||
(im if self._canvas._backend=='rlPyCairo' #rlPyCairo has a from_pil method
|
||||
else (im.tobytes if hasattr(im,'tobytes') else im.tostring)()),
|
||||
srcW, srcH, 3,
|
||||
)
|
||||
|
||||
def drawCircle(self, circle):
|
||||
c = self._canvas
|
||||
c.circle(circle.cx,circle.cy, circle.r)
|
||||
c.fillstrokepath()
|
||||
|
||||
def drawPolyLine(self, polyline, _doClose=0):
|
||||
P = polyline.points
|
||||
assert len(P) >= 2, 'Polyline must have 1 or more points'
|
||||
c = self._canvas
|
||||
c.pathBegin()
|
||||
c.moveTo(P[0], P[1])
|
||||
for i in range(2, len(P), 2):
|
||||
c.lineTo(P[i], P[i+1])
|
||||
if _doClose:
|
||||
c.pathClose()
|
||||
c.pathFill()
|
||||
c.pathStroke()
|
||||
|
||||
def drawEllipse(self, ellipse):
|
||||
c=self._canvas
|
||||
c.ellipse(ellipse.cx, ellipse.cy, ellipse.rx,ellipse.ry)
|
||||
c.fillstrokepath()
|
||||
|
||||
def drawPolygon(self, polygon):
|
||||
self.drawPolyLine(polygon,_doClose=1)
|
||||
|
||||
def drawString(self, stringObj):
|
||||
canv = self._canvas
|
||||
fill = canv.fillColor
|
||||
textRenderMode = getattr(stringObj,'textRenderMode',0)
|
||||
if fill is not None or textRenderMode:
|
||||
S = self._tracker.getState()
|
||||
text_anchor = S['textAnchor']
|
||||
fontName = S['fontName']
|
||||
fontSize = S['fontSize']
|
||||
text = stringObj.text
|
||||
x = stringObj.x
|
||||
y = stringObj.y
|
||||
if not text_anchor in ['start','inherited']:
|
||||
textLen = stringWidth(text, fontName,fontSize)
|
||||
if text_anchor=='end':
|
||||
x -= textLen
|
||||
elif text_anchor=='middle':
|
||||
x -= textLen/2
|
||||
elif text_anchor=='numeric':
|
||||
x -= numericXShift(text_anchor,text,textLen,fontName,fontSize,stringObj.encoding)
|
||||
else:
|
||||
raise ValueError('bad value for textAnchor '+str(text_anchor))
|
||||
oldTextRenderMode = canv.textRenderMode
|
||||
canv.textRenderMode = textRenderMode
|
||||
try:
|
||||
canv.drawString(x,y,text,_fontInfo=(fontName,fontSize))
|
||||
finally:
|
||||
canv.textRenderMode = oldTextRenderMode
|
||||
|
||||
def drawPath(self, path):
|
||||
c = self._canvas
|
||||
if path is EmptyClipPath:
|
||||
del c._clipPaths[-1]
|
||||
if c._clipPaths:
|
||||
P = c._clipPaths[-1]
|
||||
icp = P.isClipPath
|
||||
P.isClipPath = 1
|
||||
self.drawPath(P)
|
||||
P.isClipPath = icp
|
||||
else:
|
||||
c.clipPathClear()
|
||||
return
|
||||
from reportlab.graphics.shapes import _renderPath
|
||||
drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.pathClose)
|
||||
autoclose = getattr(path,'autoclose','')
|
||||
def rP(forceClose=False):
|
||||
c.pathBegin()
|
||||
return _renderPath(path, drawFuncs, forceClose=forceClose)
|
||||
if path.isClipPath:
|
||||
rP()
|
||||
c.clipPathSet()
|
||||
c._clipPaths.append(path)
|
||||
fill = c.fillColor is not None
|
||||
stroke = c.strokeColor is not None
|
||||
fillMode = getattr(path,'fillMode',-1)
|
||||
if autoclose=='svg':
|
||||
if fill and stroke:
|
||||
rP(forceClose=True)
|
||||
c.pathFill(fillMode)
|
||||
rP()
|
||||
c.pathStroke()
|
||||
elif fill:
|
||||
rP(forceClose=True)
|
||||
c.pathFill(fillMode)
|
||||
elif stroke:
|
||||
rP()
|
||||
c.pathStroke()
|
||||
elif autoclose=='pdf':
|
||||
rP(forceClose=True)
|
||||
if fill:
|
||||
c.pathFill(fillMode)
|
||||
if stroke:
|
||||
c.pathStroke()
|
||||
else:
|
||||
if rP():
|
||||
c.pathFill(fillMode)
|
||||
c.pathStroke()
|
||||
|
||||
def _convert2pilp(im):
|
||||
Image = _getImage()
|
||||
return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE)
|
||||
|
||||
def _convert2pilL(im):
|
||||
return im.convert("L")
|
||||
|
||||
def _convert2pil1(im):
|
||||
return im.convert("1")
|
||||
|
||||
def _saveAsPICT(im,fn,fmt,transparent=None):
|
||||
im = _convert2pilp(im)
|
||||
cols, rows = im.size
|
||||
s = _pmBackend.pil2pict(cols,rows,(im.tobytes if hasattr(im,'tobytes') else im.tostring)(),im.im.getpalette())
|
||||
if not hasattr(fn,'write'):
|
||||
with open(os.path.splitext(fn)[0]+'.'+fmt.lower(),'wb') as f:
|
||||
f.write(s)
|
||||
if os.name=='mac':
|
||||
from reportlab.lib.utils import markfilename
|
||||
markfilename(fn,ext='PICT')
|
||||
else:
|
||||
fn.write(s)
|
||||
|
||||
_pycairoFmtsMap = dict(ARGB='ARGB32',RGBA='ARGB32',RGB='RGB24')
|
||||
BEZIER_ARC_MAGIC = 0.5522847498 #constant for drawing circular arcs w/ Beziers
|
||||
class PMCanvas:
|
||||
def __init__(self,w,h,dpi=72,bg=0xffffff,configPIL=None,backend=None,
|
||||
backendFmt='RGB'):
|
||||
'''configPIL dict is passed to image save method'''
|
||||
scale = dpi/72.0
|
||||
w = int(w*scale+0.5)
|
||||
h = int(h*scale+0.5)
|
||||
self.__dict__['_gs'] = self._getGState(w,h,bg,backend,fmt=backendFmt)
|
||||
self.__dict__['_bg'] = bg
|
||||
self.__dict__['_baseCTM'] = (scale,0,0,scale,0,0)
|
||||
self.__dict__['_clipPaths'] = []
|
||||
self.__dict__['configPIL'] = configPIL
|
||||
self.__dict__['_dpi'] = dpi
|
||||
self.__dict__['_backend'] = 'rlPyCairo'
|
||||
self.__dict__['_backendfmt'] = backendFmt
|
||||
self.__dict__['_colorConverter'] = CairoColor if self._backend=='rlPyCairo' else Color2Hex
|
||||
self.ctm = self._baseCTM
|
||||
|
||||
@staticmethod
|
||||
def _getGState(w, h, bg, backend=None, fmt='RGB24'):
|
||||
mod = _getPMBackend(backend)
|
||||
if backend is None:
|
||||
backend = rl_config.renderPMBackend
|
||||
if 'cairo' in backend.lower():
|
||||
fmt = fmt.upper()
|
||||
fmt = _pycairoFmtsMap.get(fmt,fmt)
|
||||
try:
|
||||
return mod.GState(w,h,bg,fmt=fmt)
|
||||
except AttributeError:
|
||||
return mod.gstate(w,h,bg=bg)
|
||||
raise RuntimeError(f'Cannot obtain PM graphics state using backend {backend!r}')
|
||||
|
||||
def _drawTimeResize(self,w,h,bg=None):
|
||||
if bg is None: bg = self._bg
|
||||
self._drawing.width, self._drawing.height = w, h
|
||||
A = {'ctm':None, 'strokeWidth':None, 'strokeColor':None, 'lineCap':None, 'lineJoin':None, 'dashArray':None, 'fillColor':None}
|
||||
gs = self._gs
|
||||
fN,fS = gs.fontName, gs.fontSize
|
||||
for k in A.keys():
|
||||
A[k] = getattr(gs,k)
|
||||
del gs, self._gs
|
||||
gs = self.__dict__['_gs'] = _pmBackend.gstate(w,h,bg=bg)
|
||||
for k in A.keys():
|
||||
setattr(self,k,A[k])
|
||||
gs.setFont(fN,fS)
|
||||
|
||||
def toPIL(self):
|
||||
im = _getImage().new('RGBA' if self._backend=='rlPyCairo' and getattr(self,'_fmt')=='ARGB32' else 'RGB', size=(self._gs.width, self._gs.height))
|
||||
im.frombytes(self._gs.pixBuf)
|
||||
return im
|
||||
|
||||
def saveToFile(self,fn,fmt=None):
|
||||
im = self.toPIL()
|
||||
if fmt is None:
|
||||
if not isinstance(fn,str):
|
||||
raise ValueError("Invalid value '%s' for fn when fmt is None" % ascii(fn))
|
||||
fmt = os.path.splitext(fn)[1]
|
||||
if fmt.startswith('.'): fmt = fmt[1:]
|
||||
configPIL = self.configPIL or {}
|
||||
configPIL.setdefault('preConvertCB',None)
|
||||
preConvertCB=configPIL.pop('preConvertCB')
|
||||
if preConvertCB:
|
||||
im = preConvertCB(im)
|
||||
fmt = fmt.upper()
|
||||
if fmt in ('GIF',):
|
||||
im = _convert2pilp(im)
|
||||
elif fmt in ('TIFF','TIFFP','TIFFL','TIF','TIFF1'):
|
||||
if fmt.endswith('P'):
|
||||
im = _convert2pilp(im)
|
||||
elif fmt.endswith('L'):
|
||||
im = _convert2pilL(im)
|
||||
elif fmt.endswith('1'):
|
||||
im = _convert2pil1(im)
|
||||
fmt='TIFF'
|
||||
elif fmt in ('PCT','PICT'):
|
||||
return _saveAsPICT(im,fn,fmt,transparent=configPIL.get('transparent',None))
|
||||
elif fmt in ('PNG','BMP', 'PPM'):
|
||||
pass
|
||||
elif fmt in ('JPG','JPEG'):
|
||||
fmt = 'JPEG'
|
||||
else:
|
||||
raise RenderPMError("Unknown image kind %s" % fmt)
|
||||
if fmt=='TIFF':
|
||||
tc = configPIL.get('transparent',None)
|
||||
if tc:
|
||||
from PIL import ImageChops, Image
|
||||
T = 768*[0]
|
||||
for o, c in zip((0,256,512), tc.bitmap_rgb()):
|
||||
T[o+c] = 255
|
||||
#if isinstance(fn,str): ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])).save(fn+'_mask.gif','GIF')
|
||||
im = Image.merge('RGBA', im.split()+(ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])),))
|
||||
#if isinstance(fn,str): im.save(fn+'_masked.gif','GIF')
|
||||
for a,d in ('resolution',self._dpi),('resolution unit','inch'):
|
||||
configPIL[a] = configPIL.get(a,d)
|
||||
configPIL.setdefault('chops_invert',0)
|
||||
if configPIL.pop('chops_invert'):
|
||||
from PIL import ImageChops
|
||||
im = ImageChops.invert(im)
|
||||
configPIL.setdefault('preSaveCB',None)
|
||||
preSaveCB=configPIL.pop('preSaveCB')
|
||||
if preSaveCB:
|
||||
im = preSaveCB(im)
|
||||
im.save(fn,fmt,**configPIL)
|
||||
if not hasattr(fn,'write') and os.name=='mac':
|
||||
from reportlab.lib.utils import markfilename
|
||||
markfilename(fn,ext=fmt)
|
||||
|
||||
def saveToString(self,fmt='GIF'):
|
||||
s = BytesIO()
|
||||
self.saveToFile(s,fmt=fmt)
|
||||
return s.getvalue()
|
||||
|
||||
def _saveToBMP(self,f):
|
||||
'''
|
||||
Niki Spahiev, <niki@vintech.bg>, asserts that this is a respectable way to get BMP without PIL
|
||||
f is a file like object to which the BMP is written
|
||||
'''
|
||||
import struct
|
||||
gs = self._gs
|
||||
if self._backend=='rlPyCairo' and gs._fmt=='ARGB32': #pixBuf would have 4 bytes
|
||||
gs._fmt = 'RGB24' #force 3 bytes out until our BMP allows Alpha
|
||||
pix = gs.pixBuf
|
||||
gs._fmt = 'ARGB32'
|
||||
else:
|
||||
pix = gs.pixBuf
|
||||
width, height = gs.width, gs.height
|
||||
f.write(struct.pack('=2sLLLLLLhh24x','BM',len(pix)+54,0,54,40,width,height,1,24))
|
||||
rowb = width * 3
|
||||
for o in range(len(pix),0,-rowb):
|
||||
f.write(pix[o-rowb:o])
|
||||
f.write( '\0' * 14 )
|
||||
|
||||
def setFont(self,fontName,fontSize,leading=None):
|
||||
_setFont(self._gs,fontName,fontSize)
|
||||
|
||||
def __setattr__(self,name,value):
|
||||
setattr(self._gs,name,value)
|
||||
|
||||
def __getattr__(self,name):
|
||||
return getattr(self._gs,name)
|
||||
|
||||
def fillstrokepath(self,stroke=1,fill=1):
|
||||
if fill: self.pathFill()
|
||||
if stroke: self.pathStroke()
|
||||
|
||||
def _bezierArcSegmentCCW(self, cx,cy, rx,ry, theta0, theta1):
|
||||
"""compute the control points for a bezier arc with theta1-theta0 <= 90.
|
||||
Points are computed for an arc with angle theta increasing in the
|
||||
counter-clockwise (CCW) direction. returns a tuple with starting point
|
||||
and 3 control points of a cubic bezier curve for the curvto opertator"""
|
||||
|
||||
# Requires theta1 - theta0 <= 90 for a good approximation
|
||||
assert abs(theta1 - theta0) <= 90
|
||||
cos0 = cos(pi*theta0/180.0)
|
||||
sin0 = sin(pi*theta0/180.0)
|
||||
x0 = cx + rx*cos0
|
||||
y0 = cy + ry*sin0
|
||||
|
||||
cos1 = cos(pi*theta1/180.0)
|
||||
sin1 = sin(pi*theta1/180.0)
|
||||
|
||||
x3 = cx + rx*cos1
|
||||
y3 = cy + ry*sin1
|
||||
|
||||
dx1 = -rx * sin0
|
||||
dy1 = ry * cos0
|
||||
|
||||
#from pdfgeom
|
||||
halfAng = pi*(theta1-theta0)/(2.0 * 180.0)
|
||||
k = abs(4.0 / 3.0 * (1.0 - cos(halfAng) ) /(sin(halfAng)) )
|
||||
x1 = x0 + dx1 * k
|
||||
y1 = y0 + dy1 * k
|
||||
|
||||
dx2 = -rx * sin1
|
||||
dy2 = ry * cos1
|
||||
|
||||
x2 = x3 - dx2 * k
|
||||
y2 = y3 - dy2 * k
|
||||
return ((x0,y0), ((x1,y1), (x2,y2), (x3,y3)) )
|
||||
|
||||
def bezierArcCCW(self, cx,cy, rx,ry, theta0, theta1):
|
||||
"""return a set of control points for Bezier approximation to an arc
|
||||
with angle increasing counter clockwise. No requirement on (theta1-theta0) <= 90
|
||||
However, it must be true that theta1-theta0 > 0."""
|
||||
|
||||
# I believe this is also clockwise
|
||||
# pretty much just like Robert Kern's pdfgeom.BezierArc
|
||||
angularExtent = theta1 - theta0
|
||||
# break down the arc into fragments of <=90 degrees
|
||||
if abs(angularExtent) <= 90.0: # we just need one fragment
|
||||
angleList = [(theta0,theta1)]
|
||||
else:
|
||||
Nfrag = int( ceil( abs(angularExtent)/90.) )
|
||||
fragAngle = float(angularExtent)/ Nfrag # this could be negative
|
||||
angleList = []
|
||||
for ii in range(Nfrag):
|
||||
a = theta0 + ii * fragAngle
|
||||
b = a + fragAngle # hmm.. is I wonder if this is precise enought
|
||||
angleList.append((a,b))
|
||||
|
||||
ctrlpts = []
|
||||
for (a,b) in angleList:
|
||||
if not ctrlpts: # first time
|
||||
[(x0,y0), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b)
|
||||
ctrlpts.append(pts)
|
||||
else:
|
||||
[(tmpx,tmpy), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b)
|
||||
ctrlpts.append(pts)
|
||||
return ((x0,y0), ctrlpts)
|
||||
|
||||
def addEllipsoidalArc(self, cx,cy, rx, ry, ang1, ang2):
|
||||
"""adds an ellisesoidal arc segment to a path, with an ellipse centered
|
||||
on cx,cy and with radii (major & minor axes) rx and ry. The arc is
|
||||
drawn in the CCW direction. Requires: (ang2-ang1) > 0"""
|
||||
|
||||
((x0,y0), ctrlpts) = self.bezierArcCCW(cx,cy, rx,ry,ang1,ang2)
|
||||
|
||||
self.lineTo(x0,y0)
|
||||
for ((x1,y1), (x2,y2),(x3,y3)) in ctrlpts:
|
||||
self.curveTo(x1,y1,x2,y2,x3,y3)
|
||||
|
||||
def drawCentredString(self, x, y, text, text_anchor='middle', direction=None, shaping=False):
|
||||
self.drawString(x,y,text, text_anchor=text_anchor,direction=direction, shaping=shaping)
|
||||
|
||||
def drawRightString(self, text, x, y, direction=None):
|
||||
self.drawString(text,x,y,text_anchor='end',direction=direction)
|
||||
|
||||
def drawString(self, x, y, text, _fontInfo=None, text_anchor='left', direction=None, shaping=False):
|
||||
gs = self._gs
|
||||
gs_fontSize = gs.fontSize
|
||||
gs_fontName = gs.fontName
|
||||
if _fontInfo and _fontInfo!=(gs_fontSize,gs_fontName):
|
||||
fontName, fontSize = _fontInfo
|
||||
_setFont(gs,fontName,fontSize)
|
||||
else:
|
||||
fontName = gs_fontName
|
||||
fontSize = gs_fontSize
|
||||
|
||||
text, textLen = bidiShapedText(text,direction,fontName=fontName,fontSize=fontSize,shaping=shaping)
|
||||
|
||||
try:
|
||||
if text_anchor in ('end','middle', 'end'):
|
||||
textLen = stringWidth(text, fontName,fontSize)
|
||||
if text_anchor=='end':
|
||||
x -= textLen
|
||||
elif text_anchor=='middle':
|
||||
x -= textLen/2.
|
||||
elif text_anchor=='numeric':
|
||||
x -= numericXShift(text_anchor,text,textLen,fontName,fontSize)
|
||||
|
||||
if self._backend=='rlPyCairo':
|
||||
gs.drawString(x,y,text)
|
||||
else:
|
||||
font = getFont(fontName)
|
||||
if font._dynamicFont:
|
||||
gs.drawString(x,y,text)
|
||||
else:
|
||||
fc = font
|
||||
if not isUnicode(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[i-10:i],text[i:j],text[j:j+10]),)))
|
||||
|
||||
FT = unicode2T1(text,[font]+font.substitutionFonts)
|
||||
n = len(FT)
|
||||
nm1 = n-1
|
||||
for i in range(n):
|
||||
f, t = FT[i]
|
||||
if f!=fc:
|
||||
_setFont(gs,f.fontName,fontSize)
|
||||
fc = f
|
||||
gs.drawString(x,y,t)
|
||||
if i!=nm1:
|
||||
x += f.stringWidth(t.decode(f.encName),fontSize)
|
||||
finally:
|
||||
gs.setFont(gs_fontName,gs_fontSize)
|
||||
|
||||
def line(self,x1,y1,x2,y2):
|
||||
if self.strokeColor is not None:
|
||||
self.pathBegin()
|
||||
self.moveTo(x1,y1)
|
||||
self.lineTo(x2,y2)
|
||||
self.pathStroke()
|
||||
|
||||
def rect(self,x,y,width,height,stroke=1,fill=1):
|
||||
self.pathBegin()
|
||||
self.moveTo(x, y)
|
||||
self.lineTo(x+width, y)
|
||||
self.lineTo(x+width, y + height)
|
||||
self.lineTo(x, y + height)
|
||||
self.pathClose()
|
||||
self.fillstrokepath(stroke=stroke,fill=fill)
|
||||
|
||||
def roundRect(self, x, y, width, height, rx,ry):
|
||||
"""rect(self, x, y, width, height, rx,ry):
|
||||
Draw a rectangle if rx or rx and ry are specified the corners are
|
||||
rounded with ellipsoidal arcs determined by rx and ry
|
||||
(drawn in the counter-clockwise direction)"""
|
||||
if rx==0: rx = ry
|
||||
if ry==0: ry = rx
|
||||
x2 = x + width
|
||||
y2 = y + height
|
||||
self.pathBegin()
|
||||
self.moveTo(x+rx,y)
|
||||
self.addEllipsoidalArc(x2-rx, y+ry, rx, ry, 270, 360 )
|
||||
self.addEllipsoidalArc(x2-rx, y2-ry, rx, ry, 0, 90)
|
||||
self.addEllipsoidalArc(x+rx, y2-ry, rx, ry, 90, 180)
|
||||
self.addEllipsoidalArc(x+rx, y+ry, rx, ry, 180, 270)
|
||||
self.pathClose()
|
||||
self.fillstrokepath()
|
||||
|
||||
def circle(self, cx, cy, r):
|
||||
"add closed path circle with center cx,cy and axes r: counter-clockwise orientation"
|
||||
self.ellipse(cx,cy,r,r)
|
||||
|
||||
def ellipse(self, cx,cy,rx,ry):
|
||||
"""add closed path ellipse with center cx,cy and axes rx,ry: counter-clockwise orientation
|
||||
(remember y-axis increases downward) """
|
||||
self.pathBegin()
|
||||
# first segment
|
||||
x0 = cx + rx # (x0,y0) start pt
|
||||
y0 = cy
|
||||
|
||||
x3 = cx # (x3,y3) end pt of arc
|
||||
y3 = cy-ry
|
||||
|
||||
x1 = cx+rx
|
||||
y1 = cy-ry*BEZIER_ARC_MAGIC
|
||||
|
||||
x2 = x3 + rx*BEZIER_ARC_MAGIC
|
||||
y2 = y3
|
||||
self.moveTo(x0, y0)
|
||||
self.curveTo(x1,y1,x2,y2,x3,y3)
|
||||
# next segment
|
||||
x0 = x3
|
||||
y0 = y3
|
||||
|
||||
x3 = cx-rx
|
||||
y3 = cy
|
||||
|
||||
x1 = cx-rx*BEZIER_ARC_MAGIC
|
||||
y1 = cy-ry
|
||||
|
||||
x2 = x3
|
||||
y2 = cy- ry*BEZIER_ARC_MAGIC
|
||||
self.curveTo(x1,y1,x2,y2,x3,y3)
|
||||
# next segment
|
||||
x0 = x3
|
||||
y0 = y3
|
||||
|
||||
x3 = cx
|
||||
y3 = cy+ry
|
||||
|
||||
x1 = cx-rx
|
||||
y1 = cy+ry*BEZIER_ARC_MAGIC
|
||||
|
||||
x2 = cx -rx*BEZIER_ARC_MAGIC
|
||||
y2 = cy+ry
|
||||
self.curveTo(x1,y1,x2,y2,x3,y3)
|
||||
#last segment
|
||||
x0 = x3
|
||||
y0 = y3
|
||||
|
||||
x3 = cx+rx
|
||||
y3 = cy
|
||||
|
||||
x1 = cx+rx*BEZIER_ARC_MAGIC
|
||||
y1 = cy+ry
|
||||
|
||||
x2 = cx+rx
|
||||
y2 = cy+ry*BEZIER_ARC_MAGIC
|
||||
self.curveTo(x1,y1,x2,y2,x3,y3)
|
||||
self.pathClose()
|
||||
|
||||
def saveState(self):
|
||||
'''do nothing for compatibility'''
|
||||
pass
|
||||
|
||||
def setFillColor(self,aColor):
|
||||
self.fillColor = self._colorConverter(aColor)
|
||||
alpha = getattr(aColor,'alpha',None)
|
||||
if alpha is not None:
|
||||
self.fillOpacity = alpha
|
||||
|
||||
def setStrokeColor(self,aColor):
|
||||
self.strokeColor = self._colorConverter(aColor)
|
||||
alpha = getattr(aColor,'alpha',None)
|
||||
if alpha is not None:
|
||||
self.strokeOpacity = alpha
|
||||
|
||||
restoreState = saveState
|
||||
|
||||
# compatibility routines
|
||||
def setLineCap(self,cap):
|
||||
self.lineCap = cap
|
||||
|
||||
def setLineJoin(self,join):
|
||||
self.lineJoin = join
|
||||
|
||||
def setLineWidth(self,width):
|
||||
self.strokeWidth = width
|
||||
|
||||
def stringWidth(self, text, fontName=None, fontSize=None):
|
||||
return stringWidth(text, fontName or self._gs.fontName,
|
||||
(fontSize if fontSize is not None else self._gs.fontSize))
|
||||
def drawToPMCanvas(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend,backendFmt='RGB',**kwds):
|
||||
d = renderScaledDrawing(d)
|
||||
c = PMCanvas(d.width, d.height, dpi=dpi, bg=bg, configPIL=configPIL, backend=backend,backendFmt=backendFmt)
|
||||
draw(d, c, 0, 0, showBoundary=showBoundary,**kwds)
|
||||
return c
|
||||
|
||||
def drawToPIL(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend,backendFmt='RGB', **kwds):
|
||||
return drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary, backend=backend,backendFmt=backendFmt, **kwds).toPIL()
|
||||
|
||||
def drawToPILP(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend,backendFmt='RGB', **kwds):
|
||||
Image = _getImage()
|
||||
im = drawToPIL(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary,backend=backend,backendFmt=backendFmt, **kwds)
|
||||
return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE)
|
||||
|
||||
def drawToFile(d,fn,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend,backendFmt='RGB', **kwds):
|
||||
'''create a pixmap and draw drawing, d to it then save as a file
|
||||
configPIL dict is passed to image save method'''
|
||||
c = drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary,backend=backend,backendFmt=backendFmt, **kwds)
|
||||
c.saveToFile(fn,fmt)
|
||||
|
||||
def drawToString(d,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_,backend=rl_config.renderPMBackend,backendFmt='RGB',**kwds):
|
||||
s = BytesIO()
|
||||
drawToFile(d,s,fmt=fmt, dpi=dpi, bg=bg, configPIL=configPIL,backend=backend,backendFmt=backendFmt, **kwds)
|
||||
return s.getvalue()
|
||||
|
||||
save = drawToFile
|
||||
|
||||
def test(outDir='pmout', shout=False):
|
||||
def ext(x):
|
||||
if x=='tiff': x='tif'
|
||||
return x
|
||||
#grab all drawings from the test module and write out.
|
||||
#make a page of links in HTML to assist viewing.
|
||||
import os
|
||||
from reportlab.graphics import testshapes
|
||||
from reportlab.rl_config import verbose
|
||||
getAllTestDrawings = testshapes.getAllTestDrawings
|
||||
drawings = []
|
||||
if not os.path.isdir(outDir):
|
||||
os.mkdir(outDir)
|
||||
htmlTop = """<html><head><title>renderPM output results</title></head>
|
||||
<body>
|
||||
<h1>renderPM results of output</h1>
|
||||
"""
|
||||
htmlBottom = """</body>
|
||||
</html>
|
||||
"""
|
||||
html = [htmlTop]
|
||||
names = {}
|
||||
argv = sys.argv[1:]
|
||||
E = [a for a in argv if a.startswith('--ext=')]
|
||||
if not E:
|
||||
E = ['gif','tiff', 'png', 'jpg', 'pct', 'py', 'svg']
|
||||
else:
|
||||
for a in E:
|
||||
argv.remove(a)
|
||||
E = (','.join([a[6:] for a in E])).split(',')
|
||||
|
||||
errs = []
|
||||
import traceback
|
||||
from xml.sax.saxutils import escape
|
||||
def handleError(name,fmt):
|
||||
msg = 'Problem drawing %s fmt=%s file'%(name,fmt)
|
||||
if shout or verbose>2: print(msg)
|
||||
errs.append('<br/><h2 style="color:red">%s</h2>' % msg)
|
||||
buf = StringIO()
|
||||
traceback.print_exc(file=buf)
|
||||
errs.append('<pre>%s</pre>' % escape(buf.getvalue()))
|
||||
|
||||
#print in a loop, with their doc strings
|
||||
for (drawing, docstring, name) in getAllTestDrawings(doTTF=hasattr(_pmBackend,'ft_get_face')):
|
||||
i = names[name] = names.setdefault(name,0)+1
|
||||
if i>1: name += '.%02d' % (i-1)
|
||||
if argv and name not in argv: continue
|
||||
fnRoot = name
|
||||
w = int(drawing.width)
|
||||
h = int(drawing.height)
|
||||
html.append('<hr><h2>Drawing %s</h2>\n<pre>%s</pre>' % (name, docstring))
|
||||
|
||||
for k in E:
|
||||
if k in ['gif','png','jpg','pct']:
|
||||
html.append('<p>%s format</p>\n' % k.upper())
|
||||
try:
|
||||
filename = '%s.%s' % (fnRoot, ext(k))
|
||||
fullpath = os.path.join(outDir, filename)
|
||||
if os.path.isfile(fullpath):
|
||||
os.remove(fullpath)
|
||||
if k=='pct':
|
||||
drawToFile(drawing,fullpath,fmt=k,configPIL={'transparent':white})
|
||||
elif k in ['py','svg']:
|
||||
drawing.save(formats=['py','svg'],outDir=outDir,fnRoot=fnRoot)
|
||||
else:
|
||||
drawToFile(drawing,fullpath,fmt=k)
|
||||
if k in ['gif','png','jpg']:
|
||||
html.append('<img src="%s" border="1"><br>\n' % filename)
|
||||
elif k=='py':
|
||||
html.append('<a href="%s">python source</a><br>\n' % filename)
|
||||
elif k=='svg':
|
||||
html.append('<a href="%s">SVG</a><br>\n' % filename)
|
||||
if shout or verbose>2: print('wrote %s'%ascii(fullpath))
|
||||
except AttributeError:
|
||||
handleError(name,k)
|
||||
if os.environ.get('RL_NOEPSPREVIEW','0')=='1': drawing.__dict__['preview'] = 0
|
||||
for k in ('eps', 'pdf'):
|
||||
try:
|
||||
drawing.save(formats=[k],outDir=outDir,fnRoot=fnRoot)
|
||||
except:
|
||||
handleError(name,k)
|
||||
|
||||
if errs:
|
||||
html[0] = html[0].replace('</h1>',' <a href="#errors" style="color: red">(errors)</a></h1>')
|
||||
html.append('<a name="errors"/>')
|
||||
html.extend(errs)
|
||||
html.append(htmlBottom)
|
||||
htmlFileName = os.path.join(outDir, 'pm-index.html')
|
||||
with open(htmlFileName, 'w') as f:
|
||||
f.writelines(html)
|
||||
if sys.platform=='mac':
|
||||
from reportlab.lib.utils import markfilename
|
||||
markfilename(htmlFileName,ext='HTML')
|
||||
if shout or verbose>2: print('wrote %s' % htmlFileName)
|
||||
|
||||
if __name__=='__main__':
|
||||
test(shout=True)
|
||||
@@ -0,0 +1,973 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/renderPS.py
|
||||
__version__='3.3.0'
|
||||
__doc__="""Render drawing objects in Postscript"""
|
||||
|
||||
import math
|
||||
from io import BytesIO, StringIO
|
||||
from reportlab.pdfbase.pdfmetrics import getFont, stringWidth, unicode2T1 # for font info
|
||||
from reportlab.lib.utils import asBytes, char2int, rawBytes, asNative, isUnicode
|
||||
from reportlab.lib.rl_accel import fp_str
|
||||
from reportlab.graphics.renderbase import Renderer, getStateDelta, renderScaledDrawing
|
||||
from reportlab.graphics.shapes import STATE_DEFAULTS
|
||||
from reportlab import rl_config
|
||||
from reportlab.pdfgen.canvas import FILL_EVEN_ODD
|
||||
|
||||
_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
|
||||
|
||||
def _escape_and_limit(s):
|
||||
s = asBytes(s)
|
||||
R = []
|
||||
aR = R.append
|
||||
n = 0
|
||||
for c in s:
|
||||
c = _ESCAPEDICT[char2int(c)]
|
||||
aR(c)
|
||||
n += len(c)
|
||||
if n>=200:
|
||||
n = 0
|
||||
aR('\\\n')
|
||||
return ''.join(R)
|
||||
|
||||
# we need to create encoding vectors for each font we use, or they will
|
||||
# come out in Adobe's old StandardEncoding, which NOBODY uses.
|
||||
PS_WinAnsiEncoding="""
|
||||
/RE { %def
|
||||
findfont begin
|
||||
currentdict dup length dict begin
|
||||
{ %forall
|
||||
1 index /FID ne { def } { pop pop } ifelse
|
||||
} forall
|
||||
/FontName exch def dup length 0 ne { %if
|
||||
/Encoding Encoding 256 array copy def
|
||||
0 exch { %forall
|
||||
dup type /nametype eq { %ifelse
|
||||
Encoding 2 index 2 index put
|
||||
pop 1 add
|
||||
}{ %else
|
||||
exch pop
|
||||
} ifelse
|
||||
} forall
|
||||
} if pop
|
||||
currentdict dup end end
|
||||
/FontName get exch definefont pop
|
||||
} bind def
|
||||
|
||||
/WinAnsiEncoding [
|
||||
39/quotesingle 96/grave 128/euro 130/quotesinglbase/florin/quotedblbase
|
||||
/ellipsis/dagger/daggerdbl/circumflex/perthousand
|
||||
/Scaron/guilsinglleft/OE 145/quoteleft/quoteright
|
||||
/quotedblleft/quotedblright/bullet/endash/emdash
|
||||
/tilde/trademark/scaron/guilsinglright/oe/dotlessi
|
||||
159/Ydieresis 164/currency 166/brokenbar 168/dieresis/copyright
|
||||
/ordfeminine 172/logicalnot 174/registered/macron/ring
|
||||
177/plusminus/twosuperior/threesuperior/acute/mu
|
||||
183/periodcentered/cedilla/onesuperior/ordmasculine
|
||||
188/onequarter/onehalf/threequarters 192/Agrave/Aacute
|
||||
/Acircumflex/Atilde/Adieresis/Aring/AE/Ccedilla
|
||||
/Egrave/Eacute/Ecircumflex/Edieresis/Igrave/Iacute
|
||||
/Icircumflex/Idieresis/Eth/Ntilde/Ograve/Oacute
|
||||
/Ocircumflex/Otilde/Odieresis/multiply/Oslash
|
||||
/Ugrave/Uacute/Ucircumflex/Udieresis/Yacute/Thorn
|
||||
/germandbls/agrave/aacute/acircumflex/atilde/adieresis
|
||||
/aring/ae/ccedilla/egrave/eacute/ecircumflex
|
||||
/edieresis/igrave/iacute/icircumflex/idieresis
|
||||
/eth/ntilde/ograve/oacute/ocircumflex/otilde
|
||||
/odieresis/divide/oslash/ugrave/uacute/ucircumflex
|
||||
/udieresis/yacute/thorn/ydieresis
|
||||
] def
|
||||
"""
|
||||
|
||||
class PSCanvas:
|
||||
def __init__(self,size=(300,300), PostScriptLevel=2):
|
||||
self.width, self.height = size
|
||||
xtraState = []
|
||||
self._xtraState_push = xtraState.append
|
||||
self._xtraState_pop = xtraState.pop
|
||||
self.comments = 0
|
||||
self.code = []
|
||||
self.code_append = self.code.append
|
||||
self._sep = '\n'
|
||||
self._strokeColor = self._fillColor = self._lineWidth = \
|
||||
self._font = self._fontSize = self._lineCap = \
|
||||
self._lineJoin = self._color = None
|
||||
|
||||
self._fontsUsed = [] # track them as we go
|
||||
self.setFont(STATE_DEFAULTS['fontName'],STATE_DEFAULTS['fontSize'])
|
||||
self.setStrokeColor(STATE_DEFAULTS['strokeColor'])
|
||||
self.setLineCap(2)
|
||||
self.setLineJoin(0)
|
||||
self.setLineWidth(1)
|
||||
self.PostScriptLevel=PostScriptLevel
|
||||
self._fillMode = FILL_EVEN_ODD
|
||||
|
||||
def comment(self,msg):
|
||||
if self.comments: self.code_append('%'+msg)
|
||||
|
||||
def drawImage(self, image, x1,y1, width=None,height=None): # Postscript Level2 version
|
||||
# select between postscript level 1 or level 2
|
||||
if self.PostScriptLevel==1:
|
||||
self._drawImageLevel1(image, x1,y1, width, height)
|
||||
elif self.PostScriptLevel==2:
|
||||
self._drawImageLevel2(image, x1, y1, width, height)
|
||||
else :
|
||||
raise ValueError('Unsupported Postscript Level %s' % self.PostScriptLevel)
|
||||
|
||||
def clear(self):
|
||||
self.code_append('showpage') # ugh, this makes no sense oh well.
|
||||
|
||||
def _t1_re_encode(self):
|
||||
if not self._fontsUsed: return
|
||||
# for each font used, reencode the vectors
|
||||
C = []
|
||||
for fontName in self._fontsUsed:
|
||||
fontObj = getFont(fontName)
|
||||
if not fontObj._dynamicFont and fontObj.encName=='WinAnsiEncoding':
|
||||
C.append('WinAnsiEncoding /%s /%s RE' % (fontName, fontName))
|
||||
if C:
|
||||
C.insert(0,PS_WinAnsiEncoding)
|
||||
self.code.insert(1, self._sep.join(C))
|
||||
|
||||
def save(self,f=None):
|
||||
if not hasattr(f,'write'):
|
||||
_f = open(f,'wb')
|
||||
else:
|
||||
_f = f
|
||||
if self.code[-1]!='showpage': self.clear()
|
||||
self.code.insert(0,'''\
|
||||
%%!PS-Adobe-3.0 EPSF-3.0
|
||||
%%%%BoundingBox: 0 0 %d %d
|
||||
%%%% Initialization:
|
||||
/m {moveto} bind def
|
||||
/l {lineto} bind def
|
||||
/c {curveto} bind def
|
||||
''' % (self.width,self.height))
|
||||
|
||||
self._t1_re_encode()
|
||||
_f.write(rawBytes(self._sep.join(self.code)))
|
||||
if _f is not f:
|
||||
_f.close()
|
||||
from reportlab.lib.utils import markfilename
|
||||
markfilename(f,creatorcode='XPR3',filetype='EPSF')
|
||||
|
||||
def saveState(self):
|
||||
self._xtraState_push((self._fontCodeLoc,))
|
||||
self.code_append('gsave')
|
||||
|
||||
def restoreState(self):
|
||||
self.code_append('grestore')
|
||||
self._fontCodeLoc, = self._xtraState_pop()
|
||||
|
||||
def stringWidth(self, s, font=None, fontSize=None):
|
||||
"""Return the logical width of the string if it were drawn
|
||||
in the current font (defaults to self.font)."""
|
||||
font = font or self._font
|
||||
fontSize = fontSize or self._fontSize
|
||||
return stringWidth(s, font, fontSize)
|
||||
|
||||
def setLineCap(self,v):
|
||||
if self._lineCap!=v:
|
||||
self._lineCap = v
|
||||
self.code_append('%d setlinecap'%v)
|
||||
|
||||
def setLineJoin(self,v):
|
||||
if self._lineJoin!=v:
|
||||
self._lineJoin = v
|
||||
self.code_append('%d setlinejoin'%v)
|
||||
|
||||
def setDash(self, array=[], phase=0):
|
||||
"""Two notations. pass two numbers, or an array and phase"""
|
||||
# copied and modified from reportlab.canvas
|
||||
psoperation = "setdash"
|
||||
if isinstance(array,(float,int)):
|
||||
self.code_append('[%s %s] 0 %s' % (array, phase, psoperation))
|
||||
elif isinstance(array,(tuple,list)):
|
||||
assert phase >= 0, "phase is a length in user space"
|
||||
textarray = ' '.join(map(str, array))
|
||||
self.code_append('[%s] %s %s' % (textarray, phase, psoperation))
|
||||
|
||||
def setStrokeColor(self, color):
|
||||
self._strokeColor = color
|
||||
self.setColor(color)
|
||||
|
||||
def setColor(self, color):
|
||||
if self._color!=color:
|
||||
self._color = color
|
||||
if color:
|
||||
if hasattr(color, "cyan"):
|
||||
self.code_append('%s setcmykcolor' % fp_str(color.cyan, color.magenta, color.yellow, color.black))
|
||||
else:
|
||||
self.code_append('%s setrgbcolor' % fp_str(color.red, color.green, color.blue))
|
||||
|
||||
def setFillColor(self, color):
|
||||
self._fillColor = color
|
||||
self.setColor(color)
|
||||
|
||||
def setFillMode(self, v):
|
||||
self._fillMode = v
|
||||
|
||||
def setLineWidth(self, width):
|
||||
if width != self._lineWidth:
|
||||
self._lineWidth = width
|
||||
self.code_append('%s setlinewidth' % width)
|
||||
|
||||
def setFont(self,font,fontSize,leading=None):
|
||||
if self._font!=font or self._fontSize!=fontSize:
|
||||
self._fontCodeLoc = len(self.code)
|
||||
self._font = font
|
||||
self._fontSize = fontSize
|
||||
self.code_append('')
|
||||
|
||||
def line(self, x1, y1, x2, y2):
|
||||
if self._strokeColor != None:
|
||||
self.setColor(self._strokeColor)
|
||||
self.code_append('%s m %s l stroke' % (fp_str(x1, y1), fp_str(x2, y2)))
|
||||
|
||||
def _escape(self, s):
|
||||
'''
|
||||
return a copy of string s with special characters in postscript strings
|
||||
escaped with backslashes.
|
||||
'''
|
||||
try:
|
||||
return _escape_and_limit(s)
|
||||
except:
|
||||
raise ValueError("cannot escape %s" % ascii(s))
|
||||
|
||||
def _textOut(self, x, y, s, textRenderMode=0):
|
||||
if textRenderMode==3: return
|
||||
xy = fp_str(x,y)
|
||||
s = self._escape(s)
|
||||
|
||||
if textRenderMode==0: #the standard case
|
||||
self.setColor(self._fillColor)
|
||||
self.code_append('%s m (%s) show ' % (xy,s))
|
||||
return
|
||||
|
||||
fill = textRenderMode==0 or textRenderMode==2 or textRenderMode==4 or textRenderMode==6
|
||||
stroke = textRenderMode==1 or textRenderMode==2 or textRenderMode==5 or textRenderMode==6
|
||||
addToClip = textRenderMode>=4
|
||||
if fill and stroke:
|
||||
if self._fillColor is None:
|
||||
op = ''
|
||||
else:
|
||||
op = 'fill '
|
||||
self.setColor(self._fillColor)
|
||||
self.code_append('%s m (%s) true charpath gsave %s' % (xy,s,op))
|
||||
self.code_append('grestore ')
|
||||
if self._strokeColor is not None:
|
||||
self.setColor(self._strokeColor)
|
||||
self.code_append('stroke ')
|
||||
else: #can only be stroke alone
|
||||
self.setColor(self._strokeColor)
|
||||
self.code_append('%s m (%s) true charpath stroke ' % (xy,s))
|
||||
|
||||
def _issueT1String(self,fontObj,x,y,s, textRenderMode=0):
|
||||
fc = fontObj
|
||||
code_append = self.code_append
|
||||
fontSize = self._fontSize
|
||||
fontsUsed = self._fontsUsed
|
||||
escape = self._escape
|
||||
if not isUnicode(s):
|
||||
try:
|
||||
s = s.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],s[i-10:i],s[i:j],s[j:j+10]),)))
|
||||
|
||||
for f, t in unicode2T1(s,[fontObj]+fontObj.substitutionFonts):
|
||||
if f!=fc:
|
||||
psName = asNative(f.face.name)
|
||||
code_append('(%s) findfont %s scalefont setfont' % (psName,fp_str(fontSize)))
|
||||
if psName not in fontsUsed:
|
||||
fontsUsed.append(psName)
|
||||
fc = f
|
||||
self._textOut(x,y,t,textRenderMode)
|
||||
x += f.stringWidth(t.decode(f.encName),fontSize)
|
||||
if fontObj!=fc:
|
||||
self._font = None
|
||||
self.setFont(fontObj.face.name,fontSize)
|
||||
|
||||
def drawString(self, x, y, s, angle=0, text_anchor='left', textRenderMode=0):
|
||||
needFill = textRenderMode in (0,2,4,6)
|
||||
needStroke = textRenderMode in (1,2,5,6)
|
||||
if needFill or needStroke:
|
||||
if text_anchor!='left':
|
||||
textLen = stringWidth(s, self._font,self._fontSize)
|
||||
if text_anchor=='end':
|
||||
x -= textLen
|
||||
elif text_anchor=='middle':
|
||||
x -= textLen/2.
|
||||
elif text_anchor=='numeric':
|
||||
x -= numericXShift(text_anchor,s,textLen,self._font,self._fontSize)
|
||||
fontObj = getFont(self._font)
|
||||
if not self.code[self._fontCodeLoc]:
|
||||
psName = asNative(fontObj.face.name)
|
||||
self.code[self._fontCodeLoc]='(%s) findfont %s scalefont setfont' % (psName,fp_str(self._fontSize))
|
||||
if psName not in self._fontsUsed:
|
||||
self._fontsUsed.append(psName)
|
||||
if angle!=0:
|
||||
self.code_append('gsave %s translate %s rotate' % (fp_str(x,y),fp_str(angle)))
|
||||
x = y = 0
|
||||
oldColor = self._color
|
||||
if fontObj._dynamicFont:
|
||||
self._textOut(x, y, s, textRenderMode=textRenderMode)
|
||||
else:
|
||||
self._issueT1String(fontObj,x,y,s, textRenderMode=textRenderMode)
|
||||
self.setColor(oldColor)
|
||||
if angle!=0:
|
||||
self.code_append('grestore')
|
||||
|
||||
def drawCentredString(self, x, y, text, text_anchor='middle', textRenderMode=0):
|
||||
self.drawString(x,y,text, text_anchor=text_anchor, textRenderMode=textRenderMode)
|
||||
|
||||
def drawRightString(self, text, x, y, text_anchor='end', textRenderMode=0):
|
||||
self.drawString(text,x,y,text_anchor=text_anchor, textRenderMode=textRenderMode)
|
||||
|
||||
def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4, closed=0):
|
||||
codeline = '%s m %s curveto'
|
||||
data = (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4))
|
||||
if self._fillColor != None:
|
||||
self.setColor(self._fillColor)
|
||||
self.code_append((codeline % data) + ' eofill')
|
||||
if self._strokeColor != None:
|
||||
self.setColor(self._strokeColor)
|
||||
self.code_append((codeline % data)
|
||||
+ ((closed and ' closepath') or '')
|
||||
+ ' stroke')
|
||||
|
||||
########################################################################################
|
||||
|
||||
def rect(self, x1,y1, x2,y2, stroke=1, fill=1):
|
||||
"Draw a rectangle between x1,y1, and x2,y2"
|
||||
# Path is drawn in counter-clockwise direction"
|
||||
|
||||
x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py
|
||||
y1, y2 = min(y1,y2), max(y1, y2)
|
||||
self.polygon(((x1,y1),(x2,y1),(x2,y2),(x1,y2)), closed=1, stroke=stroke, fill = fill)
|
||||
|
||||
def roundRect(self, x1,y1, x2,y2, rx=8, ry=8):
|
||||
"""Draw a rounded rectangle between x1,y1, and x2,y2,
|
||||
with corners inset as ellipses with x radius rx and y radius ry.
|
||||
These should have x1<x2, y1<y2, rx>0, and ry>0."""
|
||||
# Path is drawn in counter-clockwise direction
|
||||
|
||||
x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py
|
||||
y1, y2 = min(y1,y2), max(y1, y2)
|
||||
|
||||
# Note: arcto command draws a line from current point to beginning of arc
|
||||
# save current matrix, translate to center of ellipse, scale by rx ry, and draw
|
||||
# a circle of unit radius in counterclockwise dir, return to original matrix
|
||||
# arguments are (cx, cy, rx, ry, startAngle, endAngle)
|
||||
ellipsePath = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s arc setmatrix'
|
||||
|
||||
# choice between newpath and moveTo beginning of arc
|
||||
# go with newpath for precision, does this violate any assumptions in code???
|
||||
rr = ['newpath'] # Round Rect code path
|
||||
a = rr.append
|
||||
# upper left corner ellipse is first
|
||||
a(ellipsePath % (x1+rx, y1+ry, rx, -ry, 90, 180))
|
||||
a(ellipsePath % (x1+rx, y2-ry, rx, -ry, 180, 270))
|
||||
a(ellipsePath % (x2-rx, y2-ry, rx, -ry, 270, 360))
|
||||
a(ellipsePath % (x2-rx, y1+ry, rx, -ry, 0, 90) )
|
||||
a('closepath')
|
||||
|
||||
self._fillAndStroke(rr)
|
||||
|
||||
def ellipse(self, x1,y1, x2,y2):
|
||||
"""Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2.
|
||||
These should have x1<x2 and y1<y2."""
|
||||
#Just invoke drawArc to actually draw the ellipse
|
||||
self.drawArc(x1,y1, x2,y2)
|
||||
|
||||
def circle(self, xc, yc, r):
|
||||
self.ellipse(xc-r,yc-r, xc+r,yc+r)
|
||||
|
||||
def drawArc(self, x1,y1, x2,y2, startAng=0, extent=360, fromcenter=0):
|
||||
"""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."""
|
||||
#calculate centre of ellipse
|
||||
#print "x1,y1,x2,y2,startAng,extent,fromcenter", x1,y1,x2,y2,startAng,extent,fromcenter
|
||||
cx, cy = (x1+x2)/2.0, (y1+y2)/2.0
|
||||
rx, ry = (x2-x1)/2.0, (y2-y1)/2.0
|
||||
|
||||
codeline = self._genArcCode(x1, y1, x2, y2, startAng, extent)
|
||||
|
||||
startAngleRadians = math.pi*startAng/180.0
|
||||
extentRadians = math.pi*extent/180.0
|
||||
endAngleRadians = startAngleRadians + extentRadians
|
||||
|
||||
codelineAppended = 0
|
||||
|
||||
# fill portion
|
||||
|
||||
if self._fillColor != None:
|
||||
self.setColor(self._fillColor)
|
||||
self.code_append(codeline)
|
||||
codelineAppended = 1
|
||||
if self._strokeColor!=None: self.code_append('gsave')
|
||||
self.lineTo(cx,cy)
|
||||
self.code_append('eofill')
|
||||
if self._strokeColor!=None: self.code_append('grestore')
|
||||
|
||||
# stroke portion
|
||||
if self._strokeColor != None:
|
||||
# this is a bit hacked up. There is certainly a better way...
|
||||
self.setColor(self._strokeColor)
|
||||
(startx, starty) = (cx+rx*math.cos(startAngleRadians), cy+ry*math.sin(startAngleRadians))
|
||||
if not codelineAppended:
|
||||
self.code_append(codeline)
|
||||
if fromcenter:
|
||||
# move to center
|
||||
self.lineTo(cx,cy)
|
||||
self.lineTo(startx, starty)
|
||||
self.code_append('closepath')
|
||||
self.code_append('stroke')
|
||||
|
||||
def _genArcCode(self, x1, y1, x2, y2, startAng, extent):
|
||||
"Calculate the path for an arc inscribed in rectangle defined by (x1,y1),(x2,y2)"
|
||||
#calculate semi-minor and semi-major axes of ellipse
|
||||
xScale = abs((x2-x1)/2.0)
|
||||
yScale = abs((y2-y1)/2.0)
|
||||
#calculate centre of ellipse
|
||||
x, y = (x1+x2)/2.0, (y1+y2)/2.0
|
||||
|
||||
codeline = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s %s setmatrix'
|
||||
|
||||
if extent >= 0:
|
||||
arc='arc'
|
||||
else:
|
||||
arc='arcn'
|
||||
data = (x,y, xScale, yScale, startAng, startAng+extent, arc)
|
||||
|
||||
return codeline % data
|
||||
|
||||
def polygon(self, p, closed=0, stroke=1, fill=1):
|
||||
assert len(p) >= 2, 'Polygon must have 2 or more points'
|
||||
|
||||
start = p[0]
|
||||
p = p[1:]
|
||||
|
||||
poly = []
|
||||
a = poly.append
|
||||
a("%s m" % fp_str(start))
|
||||
for point in p:
|
||||
a("%s l" % fp_str(point))
|
||||
if closed:
|
||||
a("closepath")
|
||||
|
||||
self._fillAndStroke(poly,stroke=stroke,fill=fill)
|
||||
|
||||
def lines(self, lineList, color=None, width=None):
|
||||
if self._strokeColor != None:
|
||||
self._setColor(self._strokeColor)
|
||||
codeline = '%s m %s l stroke'
|
||||
for line in lineList:
|
||||
self.code_append(codeline % (fp_str(line[0]),fp_str(line[1])))
|
||||
|
||||
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 closePath(self):
|
||||
self.code_append('closepath')
|
||||
|
||||
def polyLine(self, p):
|
||||
assert len(p) >= 1, 'Polyline must have 1 or more points'
|
||||
if self._strokeColor != None:
|
||||
self.setColor(self._strokeColor)
|
||||
self.moveTo(p[0][0], p[0][1])
|
||||
for t in p[1:]:
|
||||
self.lineTo(t[0], t[1])
|
||||
self.code_append('stroke')
|
||||
|
||||
def drawFigure(self, partList, closed=0):
|
||||
figureCode = []
|
||||
a = figureCode.append
|
||||
first = 1
|
||||
|
||||
for part in partList:
|
||||
op = part[0]
|
||||
args = list(part[1:])
|
||||
|
||||
if op == figureLine:
|
||||
if first:
|
||||
first = 0
|
||||
a("%s m" % fp_str(args[:2]))
|
||||
else:
|
||||
a("%s l" % fp_str(args[:2]))
|
||||
a("%s l" % fp_str(args[2:]))
|
||||
|
||||
elif op == figureArc:
|
||||
first = 0
|
||||
x1,y1,x2,y2,startAngle,extent = args[:6]
|
||||
a(self._genArcCode(x1,y1,x2,y2,startAngle,extent))
|
||||
|
||||
elif op == figureCurve:
|
||||
if first:
|
||||
first = 0
|
||||
a("%s m" % fp_str(args[:2]))
|
||||
else:
|
||||
a("%s l" % fp_str(args[:2]))
|
||||
a("%s curveto" % fp_str(args[2:]))
|
||||
else:
|
||||
raise TypeError("unknown figure operator: "+op)
|
||||
|
||||
if closed:
|
||||
a("closepath")
|
||||
self._fillAndStroke(figureCode)
|
||||
|
||||
def _fillAndStroke(self,code,clip=0,fill=1,stroke=1,fillMode=None):
|
||||
fill = self._fillColor and fill
|
||||
stroke = self._strokeColor and stroke
|
||||
if fill or stroke or clip:
|
||||
self.code.extend(code)
|
||||
if fill:
|
||||
if fillMode is None:
|
||||
fillMode = self._fillMode
|
||||
if stroke or clip: self.code_append("gsave")
|
||||
self.setColor(self._fillColor)
|
||||
self.code_append("eofill" if fillMode==FILL_EVEN_ODD else "fill")
|
||||
if stroke or clip: self.code_append("grestore")
|
||||
if stroke:
|
||||
if clip: self.code_append("gsave")
|
||||
self.setColor(self._strokeColor)
|
||||
self.code_append("stroke")
|
||||
if clip: self.code_append("grestore")
|
||||
if clip:
|
||||
self.code_append("clip")
|
||||
self.code_append("newpath")
|
||||
|
||||
def translate(self,x,y):
|
||||
self.code_append('%s translate' % fp_str(x,y))
|
||||
|
||||
def scale(self,x,y):
|
||||
self.code_append('%s scale' % fp_str(x,y))
|
||||
|
||||
def transform(self,a,b,c,d,e,f):
|
||||
self.code_append('[%s] concat' % fp_str(a,b,c,d,e,f))
|
||||
|
||||
def _drawTimeResize(self,w,h):
|
||||
'''if this is used we're probably in the wrong world'''
|
||||
self.width, self.height = w, h
|
||||
|
||||
def _drawImageLevel1(self, image, x1, y1, width=None, height=None):
|
||||
# Postscript Level1 version available for fallback mode when Level2 doesn't work
|
||||
# For now let's start with 24 bit RGB images (following piddlePDF again)
|
||||
component_depth = 8
|
||||
myimage = image.convert('RGB')
|
||||
imgwidth, imgheight = myimage.size
|
||||
if not width:
|
||||
width = imgwidth
|
||||
if not height:
|
||||
height = imgheight
|
||||
#print 'Image size (%d, %d); Draw size (%d, %d)' % (imgwidth, imgheight, width, height)
|
||||
# now I need to tell postscript how big image is
|
||||
|
||||
# "image operators assume that they receive sample data from
|
||||
# their data source in x-axis major index order. The coordinate
|
||||
# of the lower-left corner of the first sample is (0,0), of the
|
||||
# second (1,0) and so on" -PS2 ref manual p. 215
|
||||
#
|
||||
# The ImageMatrix maps unit squre of user space to boundary of the source image
|
||||
#
|
||||
|
||||
# The CurrentTransformationMatrix (CTM) maps the unit square of
|
||||
# user space to the rect...on the page that is to receive the
|
||||
# image. A common ImageMatrix is [width 0 0 -height 0 height]
|
||||
# (for a left to right, top to bottom image )
|
||||
|
||||
# first let's map the user coordinates start at offset x1,y1 on page
|
||||
|
||||
self.code.extend([
|
||||
'gsave',
|
||||
'%s %s translate' % (x1,y1), # need to start are lower left of image
|
||||
'%s %s scale' % (width,height),
|
||||
'/scanline %d 3 mul string def' % imgwidth # scanline by multiples of image width
|
||||
])
|
||||
|
||||
# now push the dimensions and depth info onto the stack
|
||||
# and push the ImageMatrix to map the source to the target rectangle (see above)
|
||||
# finally specify source (PS2 pp. 225 ) and by exmample
|
||||
self.code.extend([
|
||||
'%s %s %s' % (imgwidth, imgheight, component_depth),
|
||||
'[%s %s %s %s %s %s]' % (imgwidth, 0, 0, -imgheight, 0, imgheight),
|
||||
'{ currentfile scanline readhexstring pop } false 3',
|
||||
'colorimage '
|
||||
])
|
||||
|
||||
# data source output--now we just need to deliver a hex encode
|
||||
# series of lines of the right overall size can follow
|
||||
# piddlePDF again
|
||||
rawimage = (myimage.tobytes if hasattr(myimage,'tobytes') else myimage.tostring)()
|
||||
hex_encoded = self._AsciiHexEncode(rawimage)
|
||||
|
||||
# write in blocks of 78 chars per line
|
||||
outstream = StringIO(hex_encoded)
|
||||
|
||||
dataline = outstream.read(78)
|
||||
while dataline != "":
|
||||
self.code_append(dataline)
|
||||
dataline= outstream.read(78)
|
||||
self.code_append('% end of image data') # for clarity
|
||||
self.code_append('grestore') # return coordinates to normal
|
||||
|
||||
# end of drawImage
|
||||
def _AsciiHexEncode(self, input): # also based on piddlePDF
|
||||
"Helper function used by images"
|
||||
output = StringIO()
|
||||
for char in asBytes(input):
|
||||
output.write('%02x' % char2int(char))
|
||||
return output.getvalue()
|
||||
|
||||
def _drawImageLevel2(self, image, x1,y1, width=None,height=None): # Postscript Level2 version
|
||||
'''At present we're handling only PIL'''
|
||||
### what sort of image are we to draw
|
||||
if image.mode=='L' :
|
||||
imBitsPerComponent = 8
|
||||
imNumComponents = 1
|
||||
myimage = image
|
||||
elif image.mode == '1':
|
||||
myimage = image.convert('L')
|
||||
imNumComponents = 1
|
||||
myimage = image
|
||||
else :
|
||||
myimage = image.convert('RGB')
|
||||
imNumComponents = 3
|
||||
imBitsPerComponent = 8
|
||||
|
||||
imwidth, imheight = myimage.size
|
||||
if not width:
|
||||
width = imwidth
|
||||
if not height:
|
||||
height = imheight
|
||||
self.code.extend([
|
||||
'gsave',
|
||||
'%s %s translate' % (x1,y1), # need to start are lower left of image
|
||||
'%s %s scale' % (width,height)])
|
||||
|
||||
if imNumComponents == 3 :
|
||||
self.code_append('/DeviceRGB setcolorspace')
|
||||
elif imNumComponents == 1 :
|
||||
self.code_append('/DeviceGray setcolorspace')
|
||||
# create the image dictionary
|
||||
self.code_append("""
|
||||
<<
|
||||
/ImageType 1
|
||||
/Width %d /Height %d %% dimensions of source image
|
||||
/BitsPerComponent %d""" % (imwidth, imheight, imBitsPerComponent) )
|
||||
|
||||
if imNumComponents == 1:
|
||||
self.code_append('/Decode [0 1]')
|
||||
if imNumComponents == 3:
|
||||
self.code_append('/Decode [0 1 0 1 0 1] %% decode color values normally')
|
||||
|
||||
self.code.extend([ '/ImageMatrix [%s 0 0 %s 0 %s]' % (imwidth, -imheight, imheight),
|
||||
'/DataSource currentfile /ASCIIHexDecode filter',
|
||||
'>> % End image dictionary',
|
||||
'image'])
|
||||
# after image operator just need to dump image dat to file as hexstring
|
||||
rawimage = (myimage.tobytes if hasattr(myimage,'tobytes') else myimage.tostring)()
|
||||
hex_encoded = self._AsciiHexEncode(rawimage)
|
||||
|
||||
# write in blocks of 78 chars per line
|
||||
outstream = StringIO(hex_encoded)
|
||||
|
||||
dataline = outstream.read(78)
|
||||
while dataline != "":
|
||||
self.code_append(dataline)
|
||||
dataline= outstream.read(78)
|
||||
self.code_append('> % end of image data') # > is EOD for hex encoded filterfor clarity
|
||||
self.code_append('grestore') # return coordinates to normal
|
||||
|
||||
# renderpdf - draws them onto a canvas
|
||||
"""Usage:
|
||||
from reportlab.graphics import renderPS
|
||||
renderPS.draw(drawing, canvas, x, y)
|
||||
Execute the script to see some test drawings."""
|
||||
from reportlab.graphics.shapes import *
|
||||
|
||||
# hack so we only get warnings once each
|
||||
#warnOnce = WarnOnce()
|
||||
|
||||
# the main entry point for users...
|
||||
def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary):
|
||||
"""As it says"""
|
||||
R = _PSRenderer()
|
||||
R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary)
|
||||
|
||||
def _pointsFromList(L):
|
||||
'''
|
||||
given a list of coordinates [x0, y0, x1, y1....]
|
||||
produce a list of points [(x0,y0), (y1,y0),....]
|
||||
'''
|
||||
P=[]
|
||||
a = P.append
|
||||
for i in range(0,len(L),2):
|
||||
a((L[i],L[i+1]))
|
||||
return P
|
||||
|
||||
class _PSRenderer(Renderer):
|
||||
"""This draws onto a EPS document. It needs to be a class
|
||||
rather than a function, as some EPS-specific state tracking is
|
||||
needed outside of the state info in the SVG model."""
|
||||
|
||||
def drawNode(self, node):
|
||||
"""This is the recursive method called for each node
|
||||
in the tree"""
|
||||
self._canvas.comment('begin node %r'%node)
|
||||
color = self._canvas._color
|
||||
if not (isinstance(node, Path) and node.isClipPath):
|
||||
self._canvas.saveState()
|
||||
|
||||
#apply state changes
|
||||
deltas = getStateDelta(node)
|
||||
self._tracker.push(deltas)
|
||||
self.applyStateChanges(deltas, {})
|
||||
|
||||
#draw the object, or recurse
|
||||
self.drawNodeDispatcher(node)
|
||||
|
||||
rDeltas = self._tracker.pop()
|
||||
if not (isinstance(node, Path) and node.isClipPath):
|
||||
self._canvas.restoreState()
|
||||
self._canvas.comment('end node %r'%node)
|
||||
self._canvas._color = color
|
||||
|
||||
#restore things we might have lost (without actually doing anything).
|
||||
for k, v in rDeltas.items():
|
||||
if k in self._restores:
|
||||
setattr(self._canvas,self._restores[k],v)
|
||||
|
||||
## _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap',
|
||||
## 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font',
|
||||
## 'font_size':'_fontSize'}
|
||||
_restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap',
|
||||
'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font',
|
||||
'fontSize':'_fontSize'}
|
||||
|
||||
def drawRect(self, rect):
|
||||
if rect.rx == rect.ry == 0:
|
||||
#plain old rectangle
|
||||
self._canvas.rect(
|
||||
rect.x, rect.y,
|
||||
rect.x+rect.width, rect.y+rect.height)
|
||||
else:
|
||||
#cheat and assume ry = rx; better to generalize
|
||||
#pdfgen roundRect function. TODO
|
||||
self._canvas.roundRect(
|
||||
rect.x, rect.y,
|
||||
rect.x+rect.width, rect.y+rect.height, rect.rx, rect.ry
|
||||
)
|
||||
|
||||
def drawLine(self, line):
|
||||
if self._canvas._strokeColor:
|
||||
self._canvas.line(line.x1, line.y1, line.x2, line.y2)
|
||||
|
||||
def drawCircle(self, circle):
|
||||
self._canvas.circle( circle.cx, circle.cy, circle.r)
|
||||
|
||||
def drawWedge(self, wedge):
|
||||
yradius, radius1, yradius1 = wedge._xtraRadii()
|
||||
if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None) and not wedge.annular:
|
||||
startangledegrees = wedge.startangledegrees
|
||||
endangledegrees = wedge.endangledegrees
|
||||
centerx= wedge.centerx
|
||||
centery = wedge.centery
|
||||
radius = wedge.radius
|
||||
extent = endangledegrees - startangledegrees
|
||||
self._canvas.drawArc(centerx-radius, centery-yradius, centerx+radius, centery+yradius,
|
||||
startangledegrees, extent, fromcenter=1)
|
||||
else:
|
||||
P = wedge.asPolygon()
|
||||
if isinstance(P,Path):
|
||||
self.drawPath(P)
|
||||
else:
|
||||
self.drawPolygon(P)
|
||||
|
||||
def drawPolyLine(self, p):
|
||||
if self._canvas._strokeColor:
|
||||
self._canvas.polyLine(_pointsFromList(p.points))
|
||||
|
||||
def drawEllipse(self, ellipse):
|
||||
#need to convert to pdfgen's bounding box representation
|
||||
x1 = ellipse.cx - ellipse.rx
|
||||
x2 = ellipse.cx + ellipse.rx
|
||||
y1 = ellipse.cy - ellipse.ry
|
||||
y2 = ellipse.cy + ellipse.ry
|
||||
self._canvas.ellipse(x1,y1,x2,y2)
|
||||
|
||||
def drawPolygon(self, p):
|
||||
self._canvas.polygon(_pointsFromList(p.points), closed=1)
|
||||
|
||||
def drawString(self, stringObj):
|
||||
textRenderMode = getattr(stringObj,'textRenderMode',0)
|
||||
if self._canvas._fillColor or textRenderMode:
|
||||
S = self._tracker.getState()
|
||||
text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text
|
||||
if not text_anchor in ['start','inherited']:
|
||||
font, fontSize = S['fontName'], S['fontSize']
|
||||
textLen = stringWidth(text, font,fontSize)
|
||||
if text_anchor=='end':
|
||||
x -= textLen
|
||||
elif text_anchor=='middle':
|
||||
x -= textLen/2
|
||||
elif text_anchor=='numeric':
|
||||
x -= numericXShift(text_anchor,text,textLen,font,fontSize,encoding='winansi')
|
||||
else:
|
||||
raise ValueError('bad value for text_anchor '+str(text_anchor))
|
||||
self._canvas.drawString(x,y,text, textRenderMode=textRenderMode)
|
||||
|
||||
def drawPath(self, path, fillMode=None):
|
||||
from reportlab.graphics.shapes import _renderPath
|
||||
c = self._canvas
|
||||
drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath)
|
||||
autoclose = getattr(path,'autoclose','')
|
||||
def rP(**kwds):
|
||||
return _renderPath(path, drawFuncs, **kwds)
|
||||
if fillMode is None:
|
||||
fillMode = getattr(path,'fillMode',c._fillMode)
|
||||
fill = c._fillColor is not None
|
||||
stroke = c._strokeColor is not None
|
||||
clip = path.isClipPath
|
||||
fas = lambda **kwds: c._fillAndStroke([], fillMode=fillMode, **kwds)
|
||||
pathFill = lambda : c._fillAndStroke([], stroke=0, fillMode=fillMode)
|
||||
pathStroke = lambda : c._fillAndStroke([], fill=0)
|
||||
if autoclose=='svg':
|
||||
rP()
|
||||
fas(stroke=stroke,fill=fill,clip=clip)
|
||||
elif autoclose=='pdf':
|
||||
if fill:
|
||||
rP(forceClose=True)
|
||||
fas(stroke=stroke,fill=fill,clip=clip)
|
||||
elif stroke or clip:
|
||||
rP()
|
||||
fas(stroke=stroke,fill=0,clip=clip)
|
||||
else:
|
||||
if fill and rP(countOnly=True):
|
||||
rP()
|
||||
elif stroke or clip:
|
||||
rP()
|
||||
fas(stroke=stroke,fill=0,clip=clip)
|
||||
|
||||
def applyStateChanges(self, delta, newState):
|
||||
"""This takes a set of states, and outputs the operators
|
||||
needed to set those properties"""
|
||||
for key, value in delta.items():
|
||||
if key == 'transform':
|
||||
self._canvas.transform(value[0], value[1], value[2],
|
||||
value[3], value[4], value[5])
|
||||
elif key == 'strokeColor':
|
||||
#this has different semantics in PDF to SVG;
|
||||
#we always have a color, and either do or do
|
||||
#not apply it; in SVG one can have a 'None' color
|
||||
self._canvas.setStrokeColor(value)
|
||||
elif key == 'strokeWidth':
|
||||
self._canvas.setLineWidth(value)
|
||||
elif key == 'strokeLineCap': #0,1,2
|
||||
self._canvas.setLineCap(value)
|
||||
elif key == 'strokeLineJoin':
|
||||
self._canvas.setLineJoin(value)
|
||||
elif key == 'strokeDashArray':
|
||||
if value:
|
||||
if isinstance(value,(list,tuple)) and len(value)==2 and isinstance(value[1],(tuple,list)):
|
||||
phase = value[0]
|
||||
value = value[1]
|
||||
else:
|
||||
phase = 0
|
||||
self._canvas.setDash(value,phase)
|
||||
else:
|
||||
self._canvas.setDash()
|
||||
## elif key == 'stroke_opacity':
|
||||
## warnOnce('Stroke Opacity not supported yet')
|
||||
elif key == 'fillColor':
|
||||
#this has different semantics in PDF to SVG;
|
||||
#we always have a color, and either do or do
|
||||
#not apply it; in SVG one can have a 'None' color
|
||||
self._canvas.setFillColor(value)
|
||||
## elif key == 'fill_rule':
|
||||
## warnOnce('Fill rules not done yet')
|
||||
## elif key == 'fill_opacity':
|
||||
## warnOnce('Fill opacity not done yet')
|
||||
elif key in ['fontSize', 'fontName']:
|
||||
# both need setting together in PDF
|
||||
# one or both might be in the deltas,
|
||||
# so need to get whichever is missing
|
||||
fontname = delta.get('fontName', self._canvas._font)
|
||||
fontsize = delta.get('fontSize', self._canvas._fontSize)
|
||||
self._canvas.setFont(fontname, fontsize)
|
||||
|
||||
def drawImage(self, image):
|
||||
from reportlab.lib.utils import ImageReader
|
||||
im = ImageReader(image.path)
|
||||
self._canvas.drawImage(im._image,image.x,image.y,image.width,image.height)
|
||||
|
||||
def drawToFile(d,fn, showBoundary=rl_config.showBoundary,**kwd):
|
||||
d = renderScaledDrawing(d)
|
||||
c = PSCanvas((d.width,d.height))
|
||||
draw(d, c, 0, 0, showBoundary=showBoundary)
|
||||
c.save(fn)
|
||||
|
||||
def drawToString(d, showBoundary=rl_config.showBoundary):
|
||||
"Returns a PS as a string in memory, without touching the disk"
|
||||
s = BytesIO()
|
||||
drawToFile(d, s, showBoundary=showBoundary)
|
||||
return s.getvalue()
|
||||
|
||||
#########################################################
|
||||
#
|
||||
# test code. First, define a bunch of drawings.
|
||||
# Routine to draw them comes at the end.
|
||||
#
|
||||
#########################################################
|
||||
def test(outDir='epsout',shout=False):
|
||||
from reportlab.graphics import testshapes
|
||||
from reportlab.rl_config import verbose
|
||||
OLDFONTS = testshapes._FONTS[:]
|
||||
testshapes._FONTS[:] = ['Times-Roman','Times-Bold','Times-Italic', 'Times-BoldItalic','Courier']
|
||||
try:
|
||||
import os
|
||||
# save all drawings and their doc strings from the test file
|
||||
if not os.path.isdir(outDir):
|
||||
os.mkdir(outDir)
|
||||
#grab all drawings from the test module
|
||||
drawings = []
|
||||
|
||||
for funcname in dir(testshapes):
|
||||
if funcname[0:10] == 'getDrawing':
|
||||
func = getattr(testshapes,funcname)
|
||||
drawing = func()
|
||||
docstring = getattr(func,'__doc__','')
|
||||
drawings.append((drawing, docstring))
|
||||
|
||||
i = 0
|
||||
for (d, docstring) in drawings:
|
||||
filename = outDir + os.sep + 'renderPS_%d.eps'%i
|
||||
drawToFile(d,filename)
|
||||
if shout or verbose>2: print('renderPS test saved %s' % ascii(filename))
|
||||
i += 1
|
||||
finally:
|
||||
testshapes._FONTS[:] = OLDFONTS
|
||||
|
||||
if __name__=='__main__':
|
||||
import sys
|
||||
if len(sys.argv)>1:
|
||||
outdir = sys.argv[1]
|
||||
else:
|
||||
outdir = 'epsout'
|
||||
test(outdir,shout=True)
|
||||
@@ -0,0 +1,979 @@
|
||||
__doc__="""An experimental SVG renderer for the ReportLab graphics framework.
|
||||
|
||||
This will create SVG code from the ReportLab Graphics API (RLG).
|
||||
To read existing SVG code and convert it into ReportLab graphics
|
||||
objects download the svglib module here:
|
||||
|
||||
http://python.net/~gherman/#svglib
|
||||
"""
|
||||
|
||||
import math, sys, os, codecs, base64
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth # for font info
|
||||
from reportlab.lib.rl_accel import fp_str
|
||||
from reportlab.lib.utils import asNative
|
||||
from reportlab.graphics.renderbase import getStateDelta, Renderer, renderScaledDrawing
|
||||
from reportlab.graphics.shapes import STATE_DEFAULTS, Path, UserNode
|
||||
from reportlab.graphics.shapes import * # (only for test0)
|
||||
from reportlab import rl_config
|
||||
from reportlab.lib.utils import RLString, isUnicode, isBytes
|
||||
from reportlab.pdfgen.canvas import FILL_EVEN_ODD, FILL_NON_ZERO
|
||||
from .renderPM import _getImage
|
||||
|
||||
from xml.dom import getDOMImplementation
|
||||
|
||||
### some constants ###
|
||||
|
||||
sin = math.sin
|
||||
cos = math.cos
|
||||
pi = math.pi
|
||||
|
||||
AREA_STYLES = 'stroke-width stroke-linecap stroke stroke-opacity fill fill-opacity stroke-dasharray stroke-dashoffset fill-rule id'.split()
|
||||
LINE_STYLES = 'stroke-width stroke-linecap stroke stroke-opacity stroke-dasharray stroke-dashoffset id'.split()
|
||||
TEXT_STYLES = 'font-family font-weight font-style font-variant font-size id'.split()
|
||||
EXTRA_STROKE_STYLES = 'stroke-width stroke-linecap stroke stroke-opacity stroke-dasharray stroke-dashoffset'.split()
|
||||
EXTRA_FILL_STYLES = 'fill fill-opacity'.split()
|
||||
|
||||
### top-level user function ###
|
||||
def drawToString(d, showBoundary=rl_config.showBoundary,**kwds):
|
||||
"Returns a SVG as a string in memory, without touching the disk"
|
||||
s = StringIO()
|
||||
drawToFile(d, s, showBoundary=showBoundary,**kwds)
|
||||
return s.getvalue()
|
||||
|
||||
def drawToFile(d, fn, showBoundary=rl_config.showBoundary,**kwds):
|
||||
d = renderScaledDrawing(d)
|
||||
c = SVGCanvas((d.width, d.height),**kwds)
|
||||
draw(d, c, 0, 0, showBoundary=showBoundary)
|
||||
c.save(fn)
|
||||
|
||||
def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary):
|
||||
"""As it says."""
|
||||
r = _SVGRenderer()
|
||||
r.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary)
|
||||
|
||||
### helper functions ###
|
||||
def _pointsFromList(L):
|
||||
"""
|
||||
given a list of coordinates [x0, y0, x1, y1....]
|
||||
produce a list of points [(x0,y0), (y1,y0),....]
|
||||
"""
|
||||
|
||||
P=[]
|
||||
for i in range(0,len(L), 2):
|
||||
P.append((L[i], L[i+1]))
|
||||
|
||||
return P
|
||||
|
||||
def transformNode(doc, newTag, node=None, **attrDict):
|
||||
"""Transform a DOM node into new node and copy selected attributes.
|
||||
|
||||
Creates a new DOM node with tag name 'newTag' for document 'doc'
|
||||
and copies selected attributes from an existing 'node' as provided
|
||||
in 'attrDict'. The source 'node' can be None. Attribute values will
|
||||
be converted to strings.
|
||||
|
||||
E.g.
|
||||
|
||||
n = transformNode(doc, "node1", x="0", y="1")
|
||||
-> DOM node for <node1 x="0" y="1"/>
|
||||
|
||||
n = transformNode(doc, "node1", x=0, y=1+1)
|
||||
-> DOM node for <node1 x="0" y="2"/>
|
||||
|
||||
n = transformNode(doc, "node1", node0, x="x0", y="x0", zoo=bar())
|
||||
-> DOM node for <node1 x="[node0.x0]" y="[node0.y0]" zoo="[bar()]"/>
|
||||
"""
|
||||
|
||||
newNode = doc.createElement(newTag)
|
||||
for newAttr, attr in attrDict.items():
|
||||
sattr = str(attr)
|
||||
if not node:
|
||||
newNode.setAttribute(newAttr, sattr)
|
||||
else:
|
||||
attrVal = node.getAttribute(sattr)
|
||||
newNode.setAttribute(newAttr, attrVal or sattr)
|
||||
|
||||
return newNode
|
||||
|
||||
class EncodedWriter(list):
|
||||
'''
|
||||
EncodedWriter(encoding) assumes .write will be called with
|
||||
either unicode or utf8 encoded bytes. it will accumulate
|
||||
unicode
|
||||
'''
|
||||
BOMS = {
|
||||
'utf-32':codecs.BOM_UTF32,
|
||||
'utf-32-be':codecs.BOM_UTF32_BE,
|
||||
'utf-32-le':codecs.BOM_UTF32_LE,
|
||||
'utf-16':codecs.BOM_UTF16,
|
||||
'utf-16-be':codecs.BOM_UTF16_BE,
|
||||
'utf-16-le':codecs.BOM_UTF16_LE,
|
||||
}
|
||||
def __init__(self,encoding,bom=False):
|
||||
list.__init__(self)
|
||||
self.encoding = encoding = codecs.lookup(encoding).name
|
||||
if bom and '16' in encoding or '32' in encoding:
|
||||
self.write(self.BOMS[encoding])
|
||||
|
||||
def write(self,u):
|
||||
if isBytes(u):
|
||||
try:
|
||||
u = u.decode('utf-8')
|
||||
except:
|
||||
et, ev, tb = sys.exc_info()
|
||||
ev = str(ev)
|
||||
del et, tb
|
||||
raise ValueError("String %r not encoded as 'utf-8'\nerror=%s" % (u,ev))
|
||||
elif not isUnicode(u):
|
||||
raise ValueError("EncodedWriter.write(%s) argument should be 'utf-8' bytes or str" % ascii(u))
|
||||
self.append(u)
|
||||
|
||||
def getvalue(self):
|
||||
r = ''.join(self)
|
||||
del self[:]
|
||||
return r
|
||||
|
||||
_fillRuleMap = {
|
||||
FILL_NON_ZERO: 'nonzero',
|
||||
'non-zero': 'nonzero',
|
||||
'nonzero': 'nonzero',
|
||||
FILL_EVEN_ODD: 'evenodd',
|
||||
'even-odd': 'evenodd',
|
||||
'evenodd': 'evenodd',
|
||||
}
|
||||
|
||||
def py_fp_str(*args):
|
||||
return ' '.join((('%f' % a).rstrip('0').rstrip('.') for a in args))
|
||||
|
||||
### classes ###
|
||||
class SVGCanvas:
|
||||
def __init__(self, size=(300,300), encoding='utf-8', verbose=0, bom=False, **kwds):
|
||||
'''
|
||||
verbose = 0 >0 means do verbose stuff
|
||||
useClip = False True means don't use a clipPath definition put the global clip into the clip property
|
||||
to get around an issue with safari
|
||||
extraXmlDecl = '' use to add extra xml declarations
|
||||
scaleGroupId = '' id of an extra group to add around the drawing to allow easy scaling
|
||||
svgAttrs = {} dictionary of attributes to be applied to the svg tag itself
|
||||
fontSizer = 'px' a string unit or acallable that returns a string fontSize value
|
||||
'''
|
||||
self.verbose = verbose
|
||||
self.encoding = codecs.lookup(encoding).name
|
||||
self.bom = bom
|
||||
useClip = kwds.pop('useClip',False)
|
||||
self.fontHacks = kwds.pop('fontHacks',{})
|
||||
fz = kwds.pop('fontSizer','px')
|
||||
if isinstance(fz,str):
|
||||
self.fontSizer = lambda v: f'%s{fz}' % v
|
||||
elif callable(fz):
|
||||
self.fontSizer = fz
|
||||
else:
|
||||
raise ValueError(f'{fontSizer=} should be a str unit eg px/pt or a callable that returns a string')
|
||||
self.extraXmlDecl = kwds.pop('extraXmlDecl','')
|
||||
scaleGroupId = kwds.pop('scaleGroupId','')
|
||||
self._fillMode = FILL_EVEN_ODD
|
||||
|
||||
self.width, self.height = self.size = size
|
||||
# self.height = size[1]
|
||||
self.code = []
|
||||
self.style = {}
|
||||
self.path = ''
|
||||
self._strokeColor = self._fillColor = self._lineWidth = \
|
||||
self._font = self._fontSize = self._lineCap = \
|
||||
self._lineJoin = None
|
||||
if kwds.pop('use_fp_str',False):
|
||||
self.fp_str = fp_str
|
||||
else:
|
||||
self.fp_str = py_fp_str
|
||||
self.cfp_str = lambda *args: self.fp_str(*args).replace(' ',',')
|
||||
|
||||
implementation = getDOMImplementation('minidom')
|
||||
#Based on official example here http://www.w3.org/TR/SVG10/linking.html want:
|
||||
#<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
# "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
#Thus,
|
||||
#doctype = implementation.createDocumentType("svg",
|
||||
# "-//W3C//DTD SVG 20010904//EN",
|
||||
# "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd")
|
||||
#
|
||||
#However, putting that example through http://validator.w3.org/ recommends:
|
||||
#<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN"
|
||||
# "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
#So we'll use that for our SVG 1.0 output.
|
||||
doctype = implementation.createDocumentType("svg",
|
||||
"-//W3C//DTD SVG 1.0//EN",
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd")
|
||||
self.doc = implementation.createDocument(None,"svg",doctype)
|
||||
self.svg = self.doc.documentElement
|
||||
svgAttrs = dict(
|
||||
width = str(size[0]),
|
||||
height=str(self.height),
|
||||
preserveAspectRatio="xMinYMin meet",
|
||||
viewBox="0 0 %d %d" % (self.width, self.height),
|
||||
#baseProfile = "full", #disliked in V 1.0
|
||||
|
||||
#these suggested by Tim Roberts, as updated by peter@maubp.freeserve.co.uk
|
||||
xmlns="http://www.w3.org/2000/svg",
|
||||
version="1.0",
|
||||
)
|
||||
svgAttrs['fill-rule'] = _fillRuleMap[self._fillMode]
|
||||
svgAttrs["xmlns:xlink"] = "http://www.w3.org/1999/xlink"
|
||||
svgAttrs.update(kwds.pop('svgAttrs',{}))
|
||||
for k,v in svgAttrs.items():
|
||||
self.svg.setAttribute(k,v)
|
||||
|
||||
title = self.doc.createElement('title')
|
||||
text = self.doc.createTextNode('...')
|
||||
title.appendChild(text)
|
||||
self.svg.appendChild(title)
|
||||
|
||||
desc = self.doc.createElement('desc')
|
||||
text = self.doc.createTextNode('...')
|
||||
desc.appendChild(text)
|
||||
self.svg.appendChild(desc)
|
||||
|
||||
self.setFont(STATE_DEFAULTS['fontName'], STATE_DEFAULTS['fontSize'])
|
||||
self.setStrokeColor(STATE_DEFAULTS['strokeColor'])
|
||||
self.setLineCap(2)
|
||||
self.setLineJoin(0)
|
||||
self.setLineWidth(1)
|
||||
|
||||
if not useClip:
|
||||
# Add a rectangular clipping path identical to view area.
|
||||
clipPath = transformNode(self.doc, "clipPath", id="clip")
|
||||
clipRect = transformNode(self.doc, "rect", x=0, y=0,
|
||||
width=self.width, height=self.height)
|
||||
clipPath.appendChild(clipRect)
|
||||
self.svg.appendChild(clipPath)
|
||||
gtkw = dict(style="clip-path: url(#clip)")
|
||||
else:
|
||||
gtkw = dict(clip="0 0 %d %d" % (self.width,self.height))
|
||||
|
||||
self.groupTree = transformNode(self.doc, "g",
|
||||
id="group",
|
||||
transform="scale(1,-1) translate(0,-%d)" % self.height,
|
||||
**gtkw
|
||||
)
|
||||
|
||||
if scaleGroupId:
|
||||
self.scaleTree = transformNode(self.doc, "g", id=scaleGroupId, transform="scale(1,1)")
|
||||
self.scaleTree.appendChild(self.groupTree)
|
||||
self.svg.appendChild(self.scaleTree)
|
||||
else:
|
||||
self.svg.appendChild(self.groupTree)
|
||||
self.currGroup = self.groupTree
|
||||
|
||||
def save(self, fn=None):
|
||||
writer = EncodedWriter(self.encoding,bom=self.bom)
|
||||
self.doc.writexml(writer,addindent="\t",newl="\n",encoding=self.encoding)
|
||||
|
||||
if hasattr(fn,'write'):
|
||||
f = fn
|
||||
else:
|
||||
f = open(fn, 'w',encoding=self.encoding)
|
||||
|
||||
svg = writer.getvalue()
|
||||
exd = self.extraXmlDecl
|
||||
if exd:
|
||||
svg = svg.replace('?>','?>'+exd)
|
||||
f.write(svg)
|
||||
if f is not fn:
|
||||
f.close()
|
||||
|
||||
### helpers ###
|
||||
def NOTUSED_stringWidth(self, s, font=None, fontSize=None):
|
||||
"""Return the logical width of the string if it were drawn
|
||||
in the current font (defaults to self.font).
|
||||
"""
|
||||
|
||||
font = font or self._font
|
||||
fontSize = fontSize or self._fontSize
|
||||
|
||||
return stringWidth(s, font, fontSize)
|
||||
|
||||
def _formatStyle(self, include=[], exclude='',**kwds):
|
||||
style = self.style.copy()
|
||||
style.update(kwds)
|
||||
keys = list(style.keys())
|
||||
if include:
|
||||
keys = [k for k in keys if k in include]
|
||||
if exclude:
|
||||
exclude = exclude.split()
|
||||
items = [k+': '+str(style[k]) for k in keys if k not in exclude]
|
||||
else:
|
||||
items = [k+': '+str(style[k]) for k in keys]
|
||||
return '; '.join(items) + ';'
|
||||
|
||||
def _escape(self, s):
|
||||
'''I don't think this was ever needed; seems to have been copied from renderPS'''
|
||||
return s
|
||||
|
||||
def _genArcCode(self, x1, y1, x2, y2, startAng, extent):
|
||||
"""Calculate the path for an arc inscribed in rectangle defined
|
||||
by (x1,y1),(x2,y2)."""
|
||||
|
||||
return
|
||||
|
||||
#calculate semi-minor and semi-major axes of ellipse
|
||||
xScale = abs((x2-x1)/2.0)
|
||||
yScale = abs((y2-y1)/2.0)
|
||||
#calculate centre of ellipse
|
||||
x, y = (x1+x2)/2.0, (y1+y2)/2.0
|
||||
|
||||
codeline = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s %s setmatrix'
|
||||
|
||||
if extent >= 0:
|
||||
arc='arc'
|
||||
else:
|
||||
arc='arcn'
|
||||
data = (x,y, xScale, yScale, startAng, startAng+extent, arc)
|
||||
|
||||
return codeline % data
|
||||
|
||||
def _fillAndStroke(self, code, clip=0, link_info=None,styles=AREA_STYLES,fillMode=None):
|
||||
xtra = {}
|
||||
if fillMode:
|
||||
xtra['fill-rule'] = _fillRuleMap[fillMode]
|
||||
path = transformNode(self.doc, "path",
|
||||
d=self.path, style=self._formatStyle(styles),
|
||||
)
|
||||
if link_info :
|
||||
path = self._add_link(path, link_info)
|
||||
self.currGroup.appendChild(path)
|
||||
self.path = ''
|
||||
|
||||
|
||||
### styles ###
|
||||
def setLineCap(self, v):
|
||||
vals = {0:'butt', 1:'round', 2:'square'}
|
||||
if self._lineCap != v:
|
||||
self._lineCap = v
|
||||
self.style['stroke-linecap'] = vals[v]
|
||||
|
||||
def setLineJoin(self, v):
|
||||
vals = {0:'miter', 1:'round', 2:'bevel'}
|
||||
if self._lineJoin != v:
|
||||
self._lineJoin = v
|
||||
self.style['stroke-linecap'] = vals[v]
|
||||
|
||||
def setDash(self, array=[], phase=0):
|
||||
"""Two notations. Pass two numbers, or an array and phase."""
|
||||
|
||||
if isinstance(array,(float,int)):
|
||||
self.style['stroke-dasharray'] = ', '.join(map(str, ([array, phase])))
|
||||
elif isinstance(array,(tuple,list)) and len(array) > 0:
|
||||
assert phase >= 0, "phase is a length in user space"
|
||||
self.style['stroke-dasharray'] = ', '.join(map(str, array))
|
||||
if phase>0:
|
||||
self.style['stroke-dashoffset'] = str(phase)
|
||||
|
||||
def setStrokeColor(self, color):
|
||||
self._strokeColor = color
|
||||
if color == None:
|
||||
self.style['stroke'] = 'none'
|
||||
else:
|
||||
r, g, b = color.red, color.green, color.blue
|
||||
self.style['stroke'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100)
|
||||
alpha = color.normalizedAlpha
|
||||
if alpha!=1:
|
||||
self.style['stroke-opacity'] = '%s' % alpha
|
||||
elif 'stroke-opacity' in self.style:
|
||||
del self.style['stroke-opacity']
|
||||
|
||||
def setFillColor(self, color):
|
||||
self._fillColor = color
|
||||
if color == None:
|
||||
self.style['fill'] = 'none'
|
||||
else:
|
||||
r, g, b = color.red, color.green, color.blue
|
||||
self.style['fill'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100)
|
||||
alpha = color.normalizedAlpha
|
||||
if alpha!=1:
|
||||
self.style['fill-opacity'] = '%s' % alpha
|
||||
elif 'fill-opacity' in self.style:
|
||||
del self.style['fill-opacity']
|
||||
|
||||
def setFillMode(self, v):
|
||||
self._fillMode = v
|
||||
self.style['fill-rule'] = _fillRuleMap[v]
|
||||
|
||||
def setLineWidth(self, width):
|
||||
if width != self._lineWidth:
|
||||
self._lineWidth = width
|
||||
self.style['stroke-width'] = width
|
||||
|
||||
def setFont(self, font, fontSize):
|
||||
if self._font != font or self._fontSize != fontSize:
|
||||
self._font = font
|
||||
self._fontSize = fontSize
|
||||
style = self.style
|
||||
for k in TEXT_STYLES:
|
||||
if k in style:
|
||||
del style[k]
|
||||
svgAttrs = self.fontHacks[font] if font in self.fontHacks else {}
|
||||
if isinstance(font,RLString):
|
||||
svgAttrs.update(iter(font.svgAttrs.items()))
|
||||
if svgAttrs:
|
||||
for k,v in svgAttrs.items():
|
||||
a = 'font-'+k
|
||||
if a in TEXT_STYLES:
|
||||
style[a] = v
|
||||
if 'font-family' not in style:
|
||||
style['font-family'] = font
|
||||
style['font-size'] = self.fontSizer(fontSize)
|
||||
|
||||
def _add_link(self, dom_object, link_info) :
|
||||
assert isinstance(link_info, dict)
|
||||
link = transformNode(self.doc, "a", **link_info)
|
||||
link.appendChild(dom_object)
|
||||
return link
|
||||
|
||||
### shapes ###
|
||||
def rect(self, x1,y1, x2,y2, rx=8, ry=8, link_info=None, **_svgAttrs):
|
||||
"Draw a rectangle between x1,y1 and x2,y2."
|
||||
|
||||
if self.verbose: print("+++ SVGCanvas.rect")
|
||||
|
||||
x = min(x1,x2)
|
||||
y = min(y1,y2)
|
||||
kwds = {}
|
||||
rect = transformNode(self.doc, "rect",
|
||||
x=x, y=y, width=max(x1,x2)-x, height=max(y1,y2)-y,
|
||||
style=self._formatStyle(AREA_STYLES),**_svgAttrs)
|
||||
|
||||
if link_info :
|
||||
rect = self._add_link(rect, link_info)
|
||||
|
||||
self.currGroup.appendChild(rect)
|
||||
|
||||
def roundRect(self, x1,y1, x2,y2, rx=8, ry=8, link_info=None, **_svgAttrs):
|
||||
"""Draw a rounded rectangle between x1,y1 and x2,y2.
|
||||
|
||||
Corners inset as ellipses with x-radius rx and y-radius ry.
|
||||
These should have x1<x2, y1<y2, rx>0, and ry>0.
|
||||
"""
|
||||
|
||||
rect = transformNode(self.doc, "rect",
|
||||
x=x1, y=y1, width=x2-x1, height=y2-y1, rx=rx, ry=ry,
|
||||
style=self._formatStyle(AREA_STYLES), **_svgAttrs)
|
||||
|
||||
if link_info:
|
||||
rect = self._add_link(rect, link_info)
|
||||
|
||||
self.currGroup.appendChild(rect)
|
||||
|
||||
def drawString(self, s, x, y, angle=0, link_info=None, text_anchor='left', textRenderMode=0, **_svgAttrs):
|
||||
if textRenderMode==3: return #invisible
|
||||
s = asNative(s)
|
||||
if self.verbose: print("+++ SVGCanvas.drawString")
|
||||
needFill = textRenderMode==0 or textRenderMode==2 or textRenderMode==4 or textRenderMode==6
|
||||
needStroke = textRenderMode==1 or textRenderMode==2 or textRenderMode==5 or textRenderMode==6
|
||||
|
||||
if (self._fillColor!=None and needFill) or (self._strokeColor!=None and needStroke):
|
||||
if not text_anchor in ['start', 'inherited', 'left']:
|
||||
textLen = stringWidth(s,self._font,self._fontSize)
|
||||
if text_anchor=='end':
|
||||
x -= textLen
|
||||
elif text_anchor=='middle':
|
||||
x -= textLen/2.
|
||||
elif text_anchor=='numeric':
|
||||
x -= numericXShift(text_anchor,s,textLen,self._font,self._fontSize)
|
||||
else:
|
||||
raise ValueError('bad value for text_anchor ' + str(text_anchor))
|
||||
s = self._escape(s)
|
||||
st = self._formatStyle(TEXT_STYLES)
|
||||
if angle != 0:
|
||||
st = st + " rotate(%s);" % self.fp_str(angle, x, y)
|
||||
if needFill:
|
||||
st += self._formatStyle(EXTRA_FILL_STYLES)
|
||||
else:
|
||||
st += " fill:none;"
|
||||
if needStroke:
|
||||
st += self._formatStyle(EXTRA_STROKE_STYLES)
|
||||
else:
|
||||
st += " stroke:none;"
|
||||
#if textRenderMode>=4:
|
||||
# _gstate_clipPathSetOrAddself, -1, 1, 0 /*we are adding*/
|
||||
text = transformNode(self.doc, "text",
|
||||
x=x, y=y, style=st,
|
||||
transform="translate(0,%d) scale(1,-1)" % (2*y),
|
||||
**_svgAttrs
|
||||
)
|
||||
content = self.doc.createTextNode(s)
|
||||
text.appendChild(content)
|
||||
|
||||
if link_info:
|
||||
text = self._add_link(text, link_info)
|
||||
|
||||
self.currGroup.appendChild(text)
|
||||
|
||||
def drawCentredString(self, s, x, y, angle=0, text_anchor='middle',
|
||||
link_info=None, textRenderMode=0, **_svgAttrs):
|
||||
if self.verbose: print("+++ SVGCanvas.drawCentredString")
|
||||
self.drawString(s,x,y,angle=angle, link_info=link_info, text_anchor=text_anchor,
|
||||
textRenderMode=textRenderMode, **_svgAttrs)
|
||||
|
||||
def drawRightString(self, text, x, y, angle=0,text_anchor='end',
|
||||
link_info=None, textRenderMode=0, **_svgAttrs):
|
||||
if self.verbose: print("+++ SVGCanvas.drawRightString")
|
||||
self.drawString(text,x,y,angle=angle, link_info=link_info, text_anchor=text_anchor,
|
||||
textRenderMode=textRenderMode, **_svgAttrs)
|
||||
|
||||
def comment(self, data):
|
||||
"Add a comment."
|
||||
|
||||
comment = self.doc.createComment(data)
|
||||
# self.currGroup.appendChild(comment)
|
||||
|
||||
def drawImage(self, image, x, y, width, height, embed=True):
|
||||
buf = BytesIO()
|
||||
image.save(buf,'png')
|
||||
buf = asNative(base64.b64encode(buf.getvalue()))
|
||||
self.currGroup.appendChild(
|
||||
transformNode(self.doc,'image',
|
||||
x=x,y=y,width=width,height=height,
|
||||
href="data:image/png;base64,"+buf,
|
||||
transform="matrix(%s)" % self.cfp_str(1,0,0,-1,0,height+2*y),
|
||||
)
|
||||
)
|
||||
|
||||
def line(self, x1, y1, x2, y2):
|
||||
if self._strokeColor != None:
|
||||
if 0: # something is wrong with line in my SVG viewer...
|
||||
line = transformNode(self.doc, "line",
|
||||
x=x1, y=y1, x2=x2, y2=y2,
|
||||
style=self._formatStyle(LINE_STYLES))
|
||||
self.currGroup.appendChild(line)
|
||||
path = transformNode(self.doc, "path",
|
||||
d="M %s L %s Z" % (self.cfp_str(x1,y1),self.cfp_str(x2,y2)),
|
||||
style=self._formatStyle(LINE_STYLES))
|
||||
self.currGroup.appendChild(path)
|
||||
|
||||
def ellipse(self, x1, y1, x2, y2, link_info=None):
|
||||
"""Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2.
|
||||
|
||||
These should have x1<x2 and y1<y2.
|
||||
"""
|
||||
ellipse = transformNode(self.doc, "ellipse",
|
||||
cx=(x1+x2)/2.0, cy=(y1+y2)/2.0, rx=(x2-x1)/2.0, ry=(y2-y1)/2.0,
|
||||
style=self._formatStyle(AREA_STYLES))
|
||||
|
||||
if link_info:
|
||||
ellipse = self._add_link(ellipse, link_info)
|
||||
|
||||
self.currGroup.appendChild(ellipse)
|
||||
|
||||
def circle(self, xc, yc, r, link_info=None):
|
||||
circle = transformNode(self.doc, "circle",
|
||||
cx=xc, cy=yc, r=r,
|
||||
style=self._formatStyle(AREA_STYLES))
|
||||
|
||||
if link_info:
|
||||
circle = self._add_link(circle, link_info)
|
||||
|
||||
self.currGroup.appendChild(circle)
|
||||
|
||||
def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4, closed=0):
|
||||
pass
|
||||
return
|
||||
|
||||
codeline = '%s m %s curveto'
|
||||
data = (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4))
|
||||
if self._fillColor != None:
|
||||
self.code.append((codeline % data) + ' eofill')
|
||||
if self._strokeColor != None:
|
||||
self.code.append((codeline % data)
|
||||
+ ((closed and ' closepath') or '')
|
||||
+ ' stroke')
|
||||
|
||||
def drawArc(self, x1,y1, x2,y2, startAng=0, extent=360, fromcenter=0):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
cx, cy = (x1+x2)/2.0, (y1+y2)/2.0
|
||||
rx, ry = (x2-x1)/2.0, (y2-y1)/2.0
|
||||
mx = rx * cos(startAng*pi/180) + cx
|
||||
my = ry * sin(startAng*pi/180) + cy
|
||||
ax = rx * cos((startAng+extent)*pi/180) + cx
|
||||
ay = ry * sin((startAng+extent)*pi/180) + cy
|
||||
|
||||
cfp_str = self.cfp_str
|
||||
s = [].append
|
||||
if fromcenter:
|
||||
s("M %s L %s" % (cfp_str(cx, cy), cfp_str(ax, ay)))
|
||||
|
||||
if fromcenter:
|
||||
s("A %s %d %d %d %s" % \
|
||||
(cfp_str(rx, ry), 0, extent>=180, 0, cfp_str(mx, my)))
|
||||
else:
|
||||
s("M %s A %s %d %d %d %s Z" % \
|
||||
(cfp_str(mx, my), cfp_str(rx, ry), 0, extent>=180, 0, cfp_str(mx, my)))
|
||||
|
||||
if fromcenter:
|
||||
s("L %s Z" % cfp_str(cx, cy))
|
||||
|
||||
path = transformNode(self.doc, "path",
|
||||
d=' '.join(s.__self__), style=self._formatStyle())
|
||||
self.currGroup.appendChild(path)
|
||||
|
||||
def polygon(self, points, closed=0, link_info=None):
|
||||
assert len(points) >= 2, 'Polygon must have 2 or more points'
|
||||
|
||||
if self._strokeColor!=None or self._fillColor!=None:
|
||||
pts = ', '.join([fp_str(*p) for p in points])
|
||||
polyline = transformNode(self.doc, "polygon",
|
||||
points=pts, style=self._formatStyle(AREA_STYLES))
|
||||
|
||||
if link_info:
|
||||
polyline = self._add_link(polyline, link_info)
|
||||
|
||||
self.currGroup.appendChild(polyline)
|
||||
|
||||
# self._fillAndStroke(polyCode)
|
||||
|
||||
def lines(self, lineList, color=None, width=None):
|
||||
# print "### lineList", lineList
|
||||
return
|
||||
|
||||
if self._strokeColor != None:
|
||||
codeline = '%s m %s l stroke'
|
||||
for line in lineList:
|
||||
self.code.append(codeline % (fp_str(line[0]), fp_str(line[1])))
|
||||
|
||||
def polyLine(self, points):
|
||||
assert len(points) >= 1, 'Polyline must have 1 or more points'
|
||||
|
||||
if self._strokeColor != None:
|
||||
pts = ', '.join([fp_str(*p) for p in points])
|
||||
polyline = transformNode(self.doc, "polyline",
|
||||
points=pts, style=self._formatStyle(AREA_STYLES,fill=None))
|
||||
self.currGroup.appendChild(polyline)
|
||||
|
||||
### groups ###
|
||||
def startGroup(self,attrDict=dict(transform="")):
|
||||
if self.verbose: print("+++ begin SVGCanvas.startGroup")
|
||||
currGroup = self.currGroup
|
||||
group = transformNode(self.doc, "g", **attrDict)
|
||||
currGroup.appendChild(group)
|
||||
self.currGroup = group
|
||||
if self.verbose: print("+++ end SVGCanvas.startGroup")
|
||||
return currGroup
|
||||
|
||||
def endGroup(self,currGroup):
|
||||
if self.verbose: print("+++ begin SVGCanvas.endGroup")
|
||||
self.currGroup = currGroup
|
||||
if self.verbose: print("+++ end SVGCanvas.endGroup")
|
||||
|
||||
def transform(self, a, b, c, d, e, f):
|
||||
if self.verbose: print("!!! begin SVGCanvas.transform", a, b, c, d, e, f)
|
||||
tr = self.currGroup.getAttribute("transform")
|
||||
if (a, b, c, d, e, f) != (1, 0, 0, 1, 0, 0):
|
||||
t = 'matrix(%s)' % self.cfp_str(a,b,c,d,e,f)
|
||||
self.currGroup.setAttribute("transform", "%s %s" % (tr, t))
|
||||
|
||||
def translate(self, x, y):
|
||||
if (x,y) != (0,0):
|
||||
self.currGroup.setAttribute("transform", "%s %s"
|
||||
% (self.currGroup.getAttribute("transform"),
|
||||
'translate(%s)' % self.cfp_str(x,y)))
|
||||
|
||||
def scale(self, sx, sy):
|
||||
if (sx,sy) != (1,1):
|
||||
self.currGroup.setAttribute("transform", "%s %s"
|
||||
% (self.groups[-1].getAttribute("transform"),
|
||||
'scale(%s)' % self.cfp_str(sx, sy)))
|
||||
|
||||
### paths ###
|
||||
def moveTo(self, x, y):
|
||||
self.path = self.path + 'M %s ' % self.fp_str(x, y)
|
||||
|
||||
def lineTo(self, x, y):
|
||||
self.path = self.path + 'L %s ' % self.fp_str(x, y)
|
||||
|
||||
def curveTo(self, x1, y1, x2, y2, x3, y3):
|
||||
self.path = self.path + 'C %s ' % self.fp_str(x1, y1, x2, y2, x3, y3)
|
||||
|
||||
def closePath(self):
|
||||
self.path = self.path + 'Z '
|
||||
|
||||
def saveState(self):
|
||||
pass
|
||||
|
||||
def restoreState(self):
|
||||
pass
|
||||
|
||||
class _SVGRenderer(Renderer):
|
||||
"""This draws onto an SVG document.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.verbose = 0
|
||||
|
||||
def drawNode(self, node):
|
||||
"""This is the recursive method called for each node in the tree.
|
||||
"""
|
||||
|
||||
if self.verbose: print("### begin _SVGRenderer.drawNode(%r)" % node)
|
||||
|
||||
self._canvas.comment('begin node %r'%node)
|
||||
style = self._canvas.style.copy()
|
||||
if not (isinstance(node, Path) and node.isClipPath):
|
||||
pass # self._canvas.saveState()
|
||||
|
||||
#apply state changes
|
||||
deltas = getStateDelta(node)
|
||||
self._tracker.push(deltas)
|
||||
self.applyStateChanges(deltas, {})
|
||||
|
||||
#draw the object, or recurse
|
||||
self.drawNodeDispatcher(node)
|
||||
|
||||
rDeltas = self._tracker.pop()
|
||||
if not (isinstance(node, Path) and node.isClipPath):
|
||||
pass #self._canvas.restoreState()
|
||||
self._canvas.comment('end node %r'%node)
|
||||
|
||||
#restore things we might have lost (without actually doing anything).
|
||||
for k, v in rDeltas.items():
|
||||
if k in self._restores:
|
||||
setattr(self._canvas,self._restores[k],v)
|
||||
self._canvas.style = style
|
||||
|
||||
if self.verbose: print("### end _SVGRenderer.drawNode(%r)" % node)
|
||||
|
||||
_restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap',
|
||||
'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font',
|
||||
'fontSize':'_fontSize'}
|
||||
|
||||
def _get_link_info_dict(self, obj):
|
||||
#We do not want None or False as the link, even if it is the
|
||||
#attribute's value - use the empty string instead.
|
||||
url = getattr(obj, "hrefURL", "") or ""
|
||||
title = getattr(obj, "hrefTitle", "") or ""
|
||||
if url :
|
||||
#Is it valid to have a link with no href? The XML requires
|
||||
#the xlink:href to be present, but you might just want a
|
||||
#tool tip shown (via the xlink:title attribute). Note that
|
||||
#giving an href of "" is equivalent to "the current page"
|
||||
#(a relative link saying go nowhere).
|
||||
return {"xlink:href":url, "xlink:title":title, "target":"_top"}
|
||||
#Currently of all the mainstream browsers I have tested, only Safari/webkit
|
||||
#will show SVG images embedded in HTML using a simple <img src="..." /> tag.
|
||||
#However, the links don't work (Safari 3.2.1 on the Mac).
|
||||
#
|
||||
#Therefore I use the following, which also works for Firefox, Opera, and
|
||||
#IE 6.0 with Adobe SVG Viewer 6 beta:
|
||||
#<object data="..." type="image/svg+xml" width="430" height="150" class="img">
|
||||
#
|
||||
#Once displayed, Firefox and Safari treat the SVG like a frame, and
|
||||
#by default clicking on links acts "in frame" and replaces the image.
|
||||
#Opera does what I expect, and replaces the whole page with the link.
|
||||
#
|
||||
#Therefore I use target="_top" to force the links to replace the whole page.
|
||||
#This now works as expected on Safari 3.2.1, Firefox 3.0.6, Opera 9.20.
|
||||
#Perhaps the target attribute should be an option, perhaps defaulting to
|
||||
#"_top" as used here?
|
||||
else :
|
||||
return None
|
||||
|
||||
def drawGroup(self, group):
|
||||
if self.verbose: print("### begin _SVGRenderer.drawGroup")
|
||||
|
||||
currGroup = self._canvas.startGroup()
|
||||
a, b, c, d, e, f = self._tracker.getState()['transform']
|
||||
for childNode in group.getContents():
|
||||
if isinstance(childNode, UserNode):
|
||||
node2 = childNode.provideNode()
|
||||
else:
|
||||
node2 = childNode
|
||||
self.drawNode(node2)
|
||||
self._canvas.transform(a, b, c, d, e, f)
|
||||
self._canvas.endGroup(currGroup)
|
||||
|
||||
if self.verbose: print("### end _SVGRenderer.drawGroup")
|
||||
|
||||
def drawRect(self, rect):
|
||||
link_info = self._get_link_info_dict(rect)
|
||||
svgAttrs = getattr(rect,'_svgAttrs',{})
|
||||
if rect.rx == rect.ry == 0:
|
||||
#plain old rectangle
|
||||
self._canvas.rect(
|
||||
rect.x, rect.y,
|
||||
rect.x+rect.width, rect.y+rect.height, link_info=link_info, **svgAttrs)
|
||||
else:
|
||||
#cheat and assume ry = rx; better to generalize
|
||||
#pdfgen roundRect function. TODO
|
||||
self._canvas.roundRect(
|
||||
rect.x, rect.y,
|
||||
rect.x+rect.width, rect.y+rect.height,
|
||||
rect.rx, rect.ry,
|
||||
link_info=link_info, **svgAttrs)
|
||||
|
||||
def drawString(self, stringObj):
|
||||
S = self._tracker.getState()
|
||||
text_anchor, x, y, text = S['textAnchor'], stringObj.x, stringObj.y, stringObj.text
|
||||
self._canvas.drawString(text,x,y,link_info=self._get_link_info_dict(stringObj),
|
||||
text_anchor=text_anchor, textRenderMode=getattr(stringObj,'textRenderMode',0),
|
||||
**getattr(stringObj,'_svgAttrs',{}))
|
||||
|
||||
def drawLine(self, line):
|
||||
if self._canvas._strokeColor:
|
||||
self._canvas.line(line.x1, line.y1, line.x2, line.y2)
|
||||
|
||||
def drawCircle(self, circle):
|
||||
self._canvas.circle( circle.cx, circle.cy, circle.r, link_info=self._get_link_info_dict(circle))
|
||||
|
||||
def drawWedge(self, wedge):
|
||||
yradius, radius1, yradius1 = wedge._xtraRadii()
|
||||
if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None) and not wedge.annular:
|
||||
centerx, centery, radius, startangledegrees, endangledegrees = \
|
||||
wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees
|
||||
yradius = wedge.yradius or wedge.radius
|
||||
(x1, y1) = (centerx-radius, centery-yradius)
|
||||
(x2, y2) = (centerx+radius, centery+yradius)
|
||||
extent = endangledegrees - startangledegrees
|
||||
self._canvas.drawArc(x1, y1, x2, y2, startangledegrees, extent, fromcenter=1)
|
||||
else:
|
||||
P = wedge.asPolygon()
|
||||
if isinstance(P,Path):
|
||||
self.drawPath(P)
|
||||
else:
|
||||
self.drawPolygon(P)
|
||||
|
||||
def drawPolyLine(self, p):
|
||||
if self._canvas._strokeColor:
|
||||
self._canvas.polyLine(_pointsFromList(p.points))
|
||||
|
||||
def drawEllipse(self, ellipse):
|
||||
#need to convert to pdfgen's bounding box representation
|
||||
x1 = ellipse.cx - ellipse.rx
|
||||
x2 = ellipse.cx + ellipse.rx
|
||||
y1 = ellipse.cy - ellipse.ry
|
||||
y2 = ellipse.cy + ellipse.ry
|
||||
self._canvas.ellipse(x1,y1,x2,y2, link_info=self._get_link_info_dict(ellipse))
|
||||
|
||||
def drawPolygon(self, p):
|
||||
self._canvas.polygon(_pointsFromList(p.points), closed=1, link_info=self._get_link_info_dict(p))
|
||||
|
||||
def drawPath(self, path, fillMode=FILL_EVEN_ODD):
|
||||
# print "### drawPath", path.points
|
||||
from reportlab.graphics.shapes import _renderPath
|
||||
c = self._canvas
|
||||
drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath)
|
||||
if fillMode is None:
|
||||
fillMode = getattr(path,'fillMode',FILL_EVEN_ODD)
|
||||
link_info = self._get_link_info_dict(path)
|
||||
autoclose = getattr(path,'autoclose','')
|
||||
def rP(**kwds):
|
||||
return _renderPath(path, drawFuncs, **kwds)
|
||||
if autoclose=='svg':
|
||||
rP()
|
||||
c._fillAndStroke([], clip=path.isClipPath, link_info=link_info, fillMode=fillMode)
|
||||
elif autoclose=='pdf':
|
||||
rP(forceClose=True)
|
||||
c._fillAndStroke([], clip=path.isClipPath, link_info=link_info, fillMode=fillMode)
|
||||
else:
|
||||
isClosed = rP()
|
||||
if not isClosed:
|
||||
ofc = c._fillColor
|
||||
c.setFillColor(None)
|
||||
try:
|
||||
link_info = None
|
||||
c._fillAndStroke([], clip=path.isClipPath, link_info=link_info, fillMode=fillMode)
|
||||
finally:
|
||||
c.setFillColor(ofc)
|
||||
else:
|
||||
c._fillAndStroke([], clip=path.isClipPath, link_info=link_info, fillMode=fillMode)
|
||||
|
||||
def drawImage(self, image):
|
||||
path = image.path
|
||||
if isinstance(path,str):
|
||||
if not (path and os.path.isfile(path)): return
|
||||
im = _getImage().open(path)
|
||||
elif hasattr(path,'convert'):
|
||||
im = path
|
||||
else:
|
||||
return
|
||||
srcW, srcH = im.size
|
||||
dstW, dstH = image.width, image.height
|
||||
if dstW is None: dstW = srcW
|
||||
if dstH is None: dstH = srcH
|
||||
self._canvas.drawImage(im, image.x, image.y, dstW, dstH, embed=True)
|
||||
|
||||
def applyStateChanges(self, delta, newState):
|
||||
"""This takes a set of states, and outputs the operators
|
||||
needed to set those properties"""
|
||||
|
||||
for key, value in delta.items():
|
||||
if key == 'transform':
|
||||
pass
|
||||
#self._canvas.transform(value[0], value[1], value[2], value[3], value[4], value[5])
|
||||
elif key == 'strokeColor':
|
||||
self._canvas.setStrokeColor(value)
|
||||
elif key == 'strokeWidth':
|
||||
self._canvas.setLineWidth(value)
|
||||
elif key == 'strokeLineCap': #0,1,2
|
||||
self._canvas.setLineCap(value)
|
||||
elif key == 'strokeLineJoin':
|
||||
self._canvas.setLineJoin(value)
|
||||
elif key == 'strokeDashArray':
|
||||
if value:
|
||||
if isinstance(value,(list,tuple)) and len(value)==2 and isinstance(value[1],(tuple,list)):
|
||||
phase = value[0]
|
||||
value = value[1]
|
||||
else:
|
||||
phase = 0
|
||||
self._canvas.setDash(value,phase)
|
||||
else:
|
||||
self._canvas.setDash()
|
||||
elif key == 'fillColor':
|
||||
self._canvas.setFillColor(value)
|
||||
elif key in ['fontSize', 'fontName']:
|
||||
fontname = delta.get('fontName', self._canvas._font)
|
||||
fontsize = delta.get('fontSize', self._canvas._fontSize)
|
||||
self._canvas.setFont(fontname, fontsize)
|
||||
elif key == 'fillMode':
|
||||
self._canvas.setFillMode(value)
|
||||
|
||||
def test(outDir='out-svg'):
|
||||
# print all drawings and their doc strings from the test
|
||||
# file
|
||||
if not os.path.isdir(outDir):
|
||||
os.mkdir(outDir)
|
||||
#grab all drawings from the test module
|
||||
from reportlab.graphics import testshapes
|
||||
drawings = []
|
||||
|
||||
for funcname in dir(testshapes):
|
||||
if funcname[0:10] == 'getDrawing':
|
||||
func = getattr(testshapes,funcname)
|
||||
drawing = func()
|
||||
docstring = getattr(func,'__doc__','')
|
||||
drawings.append((drawing, docstring))
|
||||
|
||||
i = 0
|
||||
for (d, docstring) in drawings:
|
||||
filename = os.path.join(outDir,'renderSVG_%d.svg' % i)
|
||||
drawToFile(d, filename)
|
||||
i += 1
|
||||
|
||||
from reportlab.graphics.testshapes import getDrawing01
|
||||
d = getDrawing01()
|
||||
drawToFile(d, os.path.join(outDir,"test.svg"))
|
||||
|
||||
from reportlab.lib.corp import RL_CorpLogo
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
|
||||
rl = RL_CorpLogo()
|
||||
d = Drawing(rl.width,rl.height)
|
||||
d.add(rl)
|
||||
drawToFile(d, os.path.join(outDir,"corplogo.svg"))
|
||||
|
||||
if __name__=='__main__':
|
||||
test()
|
||||
@@ -0,0 +1,356 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2021
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/renderbase.py
|
||||
|
||||
__version__='3.13.0'
|
||||
__doc__='''Superclass for renderers to factor out common functionality and default implementations.'''
|
||||
|
||||
from reportlab.graphics.shapes import *
|
||||
from reportlab.lib.validators import DerivedValue
|
||||
from reportlab import rl_config
|
||||
|
||||
from . transform import mmult, inverse
|
||||
|
||||
def getStateDelta(shape):
|
||||
"""Used to compute when we need to change the graphics state.
|
||||
For example, if we have two adjacent red shapes we don't need
|
||||
to set the pen color to red in between. Returns the effect
|
||||
the given shape would have on the graphics state"""
|
||||
delta = {}
|
||||
for prop, value in shape.getProperties().items():
|
||||
if prop in STATE_DEFAULTS:
|
||||
delta[prop] = value
|
||||
return delta
|
||||
|
||||
class StateTracker:
|
||||
"""Keeps a stack of transforms and state
|
||||
properties. It can contain any properties you
|
||||
want, but the keys 'transform' and 'ctm' have
|
||||
special meanings. The getCTM()
|
||||
method returns the current transformation
|
||||
matrix at any point, without needing to
|
||||
invert matrixes when you pop."""
|
||||
def __init__(self, defaults=None, defaultObj=None):
|
||||
# one stack to keep track of what changes...
|
||||
self._deltas = []
|
||||
|
||||
# and another to keep track of cumulative effects. Last one in
|
||||
# list is the current graphics state. We put one in to simplify
|
||||
# loops below.
|
||||
self._combined = []
|
||||
if defaults is None:
|
||||
defaults = STATE_DEFAULTS.copy()
|
||||
if defaultObj:
|
||||
for k in STATE_DEFAULTS.keys():
|
||||
a = 'initial'+k[:1].upper()+k[1:]
|
||||
if hasattr(defaultObj,a):
|
||||
defaults[k] = getattr(defaultObj,a)
|
||||
#ensure that if we have a transform, we have a CTM
|
||||
if 'transform' in defaults:
|
||||
defaults['ctm'] = defaults['transform']
|
||||
self._combined.append(defaults)
|
||||
|
||||
def _applyDefaultObj(self,d):
|
||||
return d
|
||||
|
||||
def push(self,delta):
|
||||
"""Take a new state dictionary of changes and push it onto
|
||||
the stack. After doing this, the combined state is accessible
|
||||
through getState()"""
|
||||
|
||||
newstate = self._combined[-1].copy()
|
||||
for key, value in delta.items():
|
||||
if key == 'transform': #do cumulative matrix
|
||||
newstate['transform'] = delta['transform']
|
||||
newstate['ctm'] = mmult(self._combined[-1]['ctm'], delta['transform'])
|
||||
#print 'statetracker transform = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['transform'])
|
||||
#print 'statetracker ctm = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['ctm'])
|
||||
|
||||
else: #just overwrite it
|
||||
newstate[key] = value
|
||||
|
||||
self._combined.append(newstate)
|
||||
self._deltas.append(delta)
|
||||
|
||||
def pop(self):
|
||||
"""steps back one, and returns a state dictionary with the
|
||||
deltas to reverse out of wherever you are. Depending
|
||||
on your back end, you may not need the return value,
|
||||
since you can get the complete state afterwards with getState()"""
|
||||
del self._combined[-1]
|
||||
newState = self._combined[-1]
|
||||
lastDelta = self._deltas[-1]
|
||||
del self._deltas[-1]
|
||||
#need to diff this against the last one in the state
|
||||
reverseDelta = {}
|
||||
#print 'pop()...'
|
||||
for key, curValue in lastDelta.items():
|
||||
#print ' key=%s, value=%s' % (key, curValue)
|
||||
prevValue = newState[key]
|
||||
if prevValue != curValue:
|
||||
#print ' state popping "%s"="%s"' % (key, curValue)
|
||||
if key == 'transform':
|
||||
reverseDelta[key] = inverse(lastDelta['transform'])
|
||||
else: #just return to previous state
|
||||
reverseDelta[key] = prevValue
|
||||
return reverseDelta
|
||||
|
||||
def getState(self):
|
||||
"returns the complete graphics state at this point"
|
||||
return self._combined[-1]
|
||||
|
||||
def getCTM(self):
|
||||
"returns the current transformation matrix at this point"""
|
||||
return self._combined[-1]['ctm']
|
||||
|
||||
def __getitem__(self,key):
|
||||
"returns the complete graphics state value of key at this point"
|
||||
return self._combined[-1][key]
|
||||
|
||||
def __setitem__(self,key,value):
|
||||
"sets the complete graphics state value of key to value"
|
||||
self._combined[-1][key] = value
|
||||
|
||||
def testStateTracker():
|
||||
print('Testing state tracker')
|
||||
defaults = {'fillColor':None, 'strokeColor':None,'fontName':None, 'transform':[1,0,0,1,0,0]}
|
||||
from reportlab.graphics.shapes import _baseGFontName
|
||||
deltas = [
|
||||
{'fillColor':'red'},
|
||||
{'fillColor':'green', 'strokeColor':'blue','fontName':_baseGFontName},
|
||||
{'transform':[0.5,0,0,0.5,0,0]},
|
||||
{'transform':[0.5,0,0,0.5,2,3]},
|
||||
{'strokeColor':'red'}
|
||||
]
|
||||
|
||||
st = StateTracker(defaults)
|
||||
print('initial:', st.getState())
|
||||
print()
|
||||
for delta in deltas:
|
||||
print('pushing:', delta)
|
||||
st.push(delta)
|
||||
print('state: ',st.getState(),'\n')
|
||||
|
||||
for delta in deltas:
|
||||
print('popping:',st.pop())
|
||||
print('state: ',st.getState(),'\n')
|
||||
|
||||
def _expandUserNode(node,canvas):
|
||||
if isinstance(node, UserNode):
|
||||
try:
|
||||
if hasattr(node,'_canvas'):
|
||||
ocanvas = 1
|
||||
else:
|
||||
node._canvas = canvas
|
||||
ocanvas = None
|
||||
onode = node
|
||||
node = node.provideNode()
|
||||
finally:
|
||||
if not ocanvas: del onode._canvas
|
||||
return node
|
||||
|
||||
def renderScaledDrawing(d):
|
||||
renderScale = d.renderScale
|
||||
if renderScale!=1.0:
|
||||
o = d
|
||||
d = d.__class__(o.width*renderScale,o.height*renderScale)
|
||||
d.__dict__ = o.__dict__.copy()
|
||||
d.scale(renderScale,renderScale)
|
||||
d.renderScale = 1.0
|
||||
return d
|
||||
|
||||
class Renderer:
|
||||
"""Virtual superclass for graphics renderers."""
|
||||
|
||||
def undefined(self, operation):
|
||||
raise ValueError("%s operation not defined at superclass class=%s" %(operation, self.__class__))
|
||||
|
||||
def draw(self, drawing, canvas, x=0, y=0, showBoundary=rl_config._unset_):
|
||||
"""This is the top level function, which draws the drawing at the given
|
||||
location. The recursive part is handled by drawNode."""
|
||||
self._tracker = StateTracker(defaultObj=drawing)
|
||||
#stash references for ease of communication
|
||||
if showBoundary is rl_config._unset_: showBoundary=rl_config.showBoundary
|
||||
self._canvas = canvas
|
||||
canvas.__dict__['_drawing'] = self._drawing = drawing
|
||||
drawing._parent = None
|
||||
try:
|
||||
#bounding box
|
||||
if showBoundary:
|
||||
if hasattr(canvas,'drawBoundary'):
|
||||
canvas.drawBoundary(showBoundary,x,y,drawing.width,drawing.height)
|
||||
else:
|
||||
canvas.rect(x, y, drawing.width, drawing.height)
|
||||
canvas.saveState()
|
||||
self.initState(x,y) #this is the push()
|
||||
self.drawNode(drawing)
|
||||
self.pop()
|
||||
canvas.restoreState()
|
||||
finally:
|
||||
#remove any circular references
|
||||
del self._canvas, self._drawing, canvas._drawing, drawing._parent, self._tracker
|
||||
|
||||
def initState(self,x,y):
|
||||
deltas = self._tracker._combined[-1]
|
||||
deltas['transform'] = tuple(list(deltas['transform'])[:4])+(x,y)
|
||||
self._tracker.push(deltas)
|
||||
self.applyStateChanges(deltas, {})
|
||||
|
||||
def pop(self):
|
||||
self._tracker.pop()
|
||||
|
||||
def drawNode(self, node):
|
||||
"""This is the recursive method called for each node
|
||||
in the tree"""
|
||||
# Undefined here, but with closer analysis probably can be handled in superclass
|
||||
self.undefined("drawNode")
|
||||
|
||||
def getStateValue(self, key):
|
||||
"""Return current state parameter for given key"""
|
||||
currentState = self._tracker._combined[-1]
|
||||
return currentState[key]
|
||||
|
||||
def fillDerivedValues(self, node):
|
||||
"""Examine a node for any values which are Derived,
|
||||
and replace them with their calculated values.
|
||||
Generally things may look at the drawing or their
|
||||
parent.
|
||||
|
||||
"""
|
||||
for key, value in node.__dict__.items():
|
||||
if isinstance(value, DerivedValue):
|
||||
#just replace with default for key?
|
||||
#print ' fillDerivedValues(%s)' % key
|
||||
newValue = value.getValue(self, key)
|
||||
#print ' got value of %s' % newValue
|
||||
node.__dict__[key] = newValue
|
||||
|
||||
def drawNodeDispatcher(self, anode):
|
||||
"""dispatch on the node's (super) class: shared code"""
|
||||
canvas = getattr(self,'_canvas',None)
|
||||
|
||||
try:
|
||||
# replace UserNode with its contents
|
||||
node = _expandUserNode(anode,canvas)
|
||||
if not node: return
|
||||
if hasattr(node,'_canvas'):
|
||||
ocanvas = 1
|
||||
else:
|
||||
node._canvas = canvas
|
||||
ocanvas = None
|
||||
nodeparent = node is not anode and not hasattr(node,'_parent')
|
||||
if nodeparent: node._parent = anode
|
||||
|
||||
self.fillDerivedValues(node)
|
||||
dtcb = getattr(node,'_drawTimeCallback',None)
|
||||
if dtcb:
|
||||
dtcb(node,canvas=canvas,renderer=self)
|
||||
#draw the object, or recurse
|
||||
if isinstance(node, Line):
|
||||
self.drawLine(node)
|
||||
elif isinstance(node, Path):
|
||||
self.drawPath(node)
|
||||
elif isinstance(node, String):
|
||||
self.drawString(node)
|
||||
elif isinstance(node, Group):
|
||||
self.drawGroup(node)
|
||||
elif isinstance(node, Rect):
|
||||
self.drawRect(node)
|
||||
elif isinstance(node, Image):
|
||||
self.drawImage(node)
|
||||
elif isinstance(node, Circle):
|
||||
self.drawCircle(node)
|
||||
elif isinstance(node, Ellipse):
|
||||
self.drawEllipse(node)
|
||||
elif isinstance(node, PolyLine):
|
||||
self.drawPolyLine(node)
|
||||
elif isinstance(node, Polygon):
|
||||
self.drawPolygon(node)
|
||||
elif isinstance(node, Wedge):
|
||||
self.drawWedge(node)
|
||||
elif isinstance(node, DirectDraw):
|
||||
node.drawDirectly(self)
|
||||
else:
|
||||
print('DrawingError','Unexpected element %s in drawing!' % str(node))
|
||||
finally:
|
||||
if not ocanvas: del node._canvas
|
||||
if nodeparent: del node._parent
|
||||
|
||||
_restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap',
|
||||
'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font',
|
||||
'font_size':'_fontSize'}
|
||||
|
||||
def drawGroup(self, group):
|
||||
# just do the contents. Some renderers might need to override this
|
||||
# if they need a flipped transform
|
||||
canvas = getattr(self,'_canvas',None)
|
||||
for anode in group.getContents():
|
||||
node = _expandUserNode(anode,canvas)
|
||||
if not node: continue
|
||||
|
||||
#here is where we do derived values - this seems to get everything. Touch wood.
|
||||
self.fillDerivedValues(node)
|
||||
try:
|
||||
if hasattr(node,'_canvas'):
|
||||
ocanvas = 1
|
||||
else:
|
||||
node._canvas = canvas
|
||||
ocanvas = None
|
||||
if node is not anode:
|
||||
anode._parent = group
|
||||
node._parent = anode
|
||||
else:
|
||||
node._parent = group
|
||||
self.drawNode(node)
|
||||
finally:
|
||||
if node is not anode: del anode._parent
|
||||
del node._parent
|
||||
if not ocanvas: del node._canvas
|
||||
|
||||
def drawWedge(self, wedge):
|
||||
# by default ask the wedge to make a polygon of itself and draw that!
|
||||
#print "drawWedge"
|
||||
P = wedge.asPolygon()
|
||||
if isinstance(P,Path):
|
||||
self.drawPath(P)
|
||||
else:
|
||||
self.drawPolygon(P)
|
||||
|
||||
def drawPath(self, path):
|
||||
polygons = path.asPolygons()
|
||||
for polygon in polygons:
|
||||
self.drawPolygon(polygon)
|
||||
|
||||
def drawRect(self, rect):
|
||||
# could be implemented in terms of polygon
|
||||
self.undefined("drawRect")
|
||||
|
||||
def drawLine(self, line):
|
||||
self.undefined("drawLine")
|
||||
|
||||
def drawCircle(self, circle):
|
||||
self.undefined("drawCircle")
|
||||
|
||||
def drawPolyLine(self, p):
|
||||
self.undefined("drawPolyLine")
|
||||
|
||||
def drawEllipse(self, ellipse):
|
||||
self.undefined("drawEllipse")
|
||||
|
||||
def drawPolygon(self, p):
|
||||
self.undefined("drawPolygon")
|
||||
|
||||
def drawString(self, stringObj):
|
||||
self.undefined("drawString")
|
||||
|
||||
def applyStateChanges(self, delta, newState):
|
||||
"""This takes a set of states, and outputs the operators
|
||||
needed to set those properties"""
|
||||
self.undefined("applyStateChanges")
|
||||
|
||||
def drawImage(self,*args,**kwds):
|
||||
raise NotImplementedError('drawImage')
|
||||
|
||||
if __name__=='__main__':
|
||||
print("this file has no script interpretation")
|
||||
print(__doc__)
|
||||
@@ -0,0 +1 @@
|
||||
__doc__="""Example drawings to review, used in autogenerated docs"""
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user