__all__=('AcroForm',)
from reportlab.pdfbase.pdfdoc import (PDFObject, PDFArray, PDFDictionary, PDFString, pdfdocEnc,
                                    PDFName, PDFStream, PDFStreamFilterZCompress, escapePDF)
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.lib.colors import Color, CMYKColor, Whiter, Blacker, opaqueColor
from reportlab.lib.rl_accel import fp_str
from reportlab.lib.utils import isStr, asNative
import weakref

_useDefault=object()

visibilities = dict(
                visible=0,
                hidden=0,
                visibleNonPrinting=0,
                hiddenPrintable=0,
                )

orientations = {
                0: [],
                90: [],
                180: [],
                270: [],
                }

#adobe counts bits 1 - 32
fieldFlagValues = dict(
                readOnly = 1<<0,
                required = 1<<1,
                noExport = 1<<2,
                noToggleToOff = 1<<14,
                radio = 1<<15,
                pushButton = 1<<16,
                radiosInUnison = 1<<25,
                #text fields
                multiline = 1<<12,
                password = 1<<13,
                fileSelect = 1<<20,         #1.4
                doNotSpellCheck = 1<<22,    #1.4
                doNotScroll = 1<<23,        #1.4
                comb = 1<<24,               #1.5
                richText = 1<<25,           #1.5

                #choice fields
                combo = 1<<17,
                edit = 1<<18,
                sort = 1<<19,
                multiSelect = 1<<21,        #1.4
                commitOnSelChange = 1<<26,  #1.5
                )

annotationFlagValues = dict(
                    invisible=1<<0,
                    hidden=1<<1,
                    nozoom=1<<3,
                    norotate=1<<4,
                    noview=1<<5,
                    readonly=1<<6,
                    locked=1<<7,            #1.4
                    togglenoview=1<<8,      #1.9
                    lockedcontents=1<<9,    #1.7
                    )
annotationFlagValues['print']=1<<2

_bsStyles = dict(
            solid='S',
            dashed='D',
            bevelled='B',
            inset='I',
            underlined='U',
            )

def bsPDF(borderWidth,borderStyle,dashLen):
    d = dict(W=borderWidth,S=PDFName(_bsStyles[borderStyle]))
    if borderStyle=='dashed':
        if not dashLen:
            dashLen = [3]
        elif not isinstance(dashLen,(list,tuple)):
            dashLen = [dashLen]
        d['D'] = PDFArray(dashLen)
    return PDFDictionary(d)

def escPDF(s):
    return escapePDF(s).replace('%','\\045')

def makeFlags(s,d=annotationFlagValues):
    if not isinstance(s,int):
        v = s
        s = 0
        for x in v.split():
            s |= d[x]
    return s

class PDFFromString(PDFObject):
    def __init__(self,s):
        if not isStr(s):
            raise ValueError('need a unicode/bytes argument not %r' % s)
        self._s = s

    def format(self,document):
        return pdfdocEnc(self._s)

class RadioGroup(PDFObject):
    def __init__(self,name,tooltip='',fieldFlags='noToggleToOff required radio'):
        if not name:
            raise ValueError('RadioGroup created with no name')
        self.TU = tooltip
        self.Ff = makeFlags(fieldFlags,fieldFlagValues)
        self.kids = []
        self.T = name
        self.V = None

    def format(self,doc):
        kids = self.kids
        d = len(kids)
        if d<2: raise ValueError('RadioGroup:%s has %d < 2 RadioBoxes' % (self.T,d))

        d = dict(
                Ff=self.Ff,
                Kids = PDFArray([k for k in self.kids]),
                FT = PDFName('Btn'),
                T = PDFString(self.T),
                #DA = PDFString('0 g'),
                )
        if self.V: d['V'] = PDFName(self.V)
        if self.TU: d['TU'] =PDFString(self.TU)
        r = PDFDictionary(d).format(doc)
        return r


def _pdfObjToStr(obj):
    if isinstance(obj,PDFArray):
        return '[%s]' % ''.join((_pdfObjToStr(e) for e in obj.sequence))
    if isinstance(obj,PDFFromString):
        return obj._s
    return str(obj)

_NotSet = object()

class AcroForm(PDFObject):
    formFontNames = {
        "Helvetica": "Helv",
        "Helvetica-Bold": "HeBo",
        'Courier': "Cour",
        'Courier-Bold': "CoBo",
        'Courier-Oblique': "CoOb",
        'Courier-BoldOblique': "CoBO",
        'Helvetica-Oblique': "HeOb",
        'Helvetica-BoldOblique': "HeBO",
        'Times-Roman': "Time",
        'Times-Bold': "TiBo",
        'Times-Italic': "TiIt",
        'Times-BoldItalic': "TiBI",
        }
    def __init__(self,canv,**kwds):
        self.referenceMap = {}
        self._canv = weakref.ref(canv)
        self.fonts = {}
        self.fields = []
        self._radios = {}
        self._refMap = {}
        self._pdfdocenc = {}
        self.sigFlags = None
        self.extras = {}

    @property
    def useDefault(self):
        return _useDefault

    @property
    def canv(self):
        _canv = self._canv()
        if _canv is None:
            raise ValueError('%s.canv is no longer available' % self.__class__.__name__)
        return _canv

    def fontRef(self,f):
        return '/Font << /%s %s >>' % (f,self.fonts[f])

    def format(self,doc):
        d = dict(
                Fields = PDFArray([self.getRef(f) for f in self.fields]),
                )
        if self.sigFlags: d['SigFlags'] = self.sigFlags
        if self.fonts:
            FK = list(sorted(self.fonts.keys()))
            F = [self.fontRef(f) for f in FK]
            d['DA'] = PDFString('/%s 0 Tf 0 g' % FK[0])
            d['DR'] = PDFFromString('<< /Encoding\n<<\n/RLAFencoding\n%s\n>>\n%s\n>>' % (self.encRefStr,'\n'.join(F)))
        d.update(self.extras)
        r = PDFDictionary(d).format(doc)
        return r

    def colorTuple(self,c):
        # ISO-32000-1, Table 189: An array of numbers that shall be in ther
        #  range 0.0 to 1.0 specifying the colour [..]. The number of array
        #  elements determines the colour space in which the colour shall
        #  be defined:
        #  0 No colour; transparent 1 DeviceGray 3 DeviceRGB 4 DeviceCMYK
        if c is None or c.alpha == 0:
            return ()
        return c.cmyk() if isinstance(c,CMYKColor) else c.rgb()

    def streamFillColor(self,c):
        t = self.colorTuple(c)
        return fp_str(*t)+(' k' if len(t)==4 else ' rg')
                    
    def streamStrokeColor(self,c):
        t = self.colorTuple(c)
        return fp_str(*t)+(' K' if len(t)==4 else ' RG')

    def checkboxAP(self,
                key,                    #N/D/R
                value,                  #Yes/Off
                buttonStyle='circle',
                shape='square',
                fillColor=None,
                borderColor=None,
                textColor=None,
                borderWidth=None,
                borderStyle='solid',
                size=20,
                dashLen=3,
                ):
        stream = [].append
        ds = size
        if shape=='square':
            stream('q')
            if opaqueColor(fillColor):
                streamFill = self.streamFillColor(fillColor)
                stream('1 g 1 G %(streamFill)s 0 0 %(size)s %(size)s re f')
            if borderWidth is not None and opaqueColor(borderColor):
                streamStroke = self.streamStrokeColor(borderColor)
                hbw = borderWidth*0.5
                smbw = size - borderWidth
                ds = smbw
                if borderStyle=='underlined':
                    stream('%(streamStroke)s %(borderWidth)s w 0 %(hbw)s m %(size)s %(hbw)s l s')
                elif borderStyle in ('dashed','inset','bevelled','solid'):
                    if borderStyle=='dashed':
                        dash = ' [%s ] 0 d' % fp_str(dashLen)
                    else:
                        dash = ''
                    stream('%(streamStroke)s%(dash)s %(borderWidth)s w %(hbw)s %(hbw)s %(smbw)s %(smbw)s re s')

                if borderStyle in ('bevelled','inset') and opaqueColor(fillColor):
                    _2bw = 2*borderWidth
                    sm2bw = size - _2bw
                    ds = sm2bw
                    bbs0 = Blacker(fillColor,0.5)
                    bbs1 = fillColor
                    if key!='D':
                        bbs0, bbs1 = bbs1, bbs0
                    bbs0 = self.streamFillColor(bbs0)
                    bbs1 = self.streamFillColor(bbs1)
                    stream('%(bbs0)s %(borderWidth)s %(borderWidth)s m %(borderWidth)s %(smbw)s l %(smbw)s %(smbw)s l %(sm2bw)s %(sm2bw)s l %(_2bw)s %(sm2bw)s l %(_2bw)s %(_2bw)s l f %(bbs1)s %(smbw)s %(smbw)s m %(smbw)s %(borderWidth)s l %(borderWidth)s %(borderWidth)s l %(_2bw)s %(_2bw)s l %(sm2bw)s %(_2bw)s l %(sm2bw)s %(sm2bw)s l f')
            stream('Q')
        elif shape=='circle':
            cas = lambda _r,**_casKwds: self.circleArcStream(size,_r,**_casKwds)
            r = size*0.5
            if opaqueColor(fillColor):
                streamFill = self.streamFillColor(fillColor)
                stream('q 1 g 1 G %(streamFill)s')
                stream(cas(r))
                stream('f')
                stream('Q')
            if borderWidth is not None and opaqueColor(borderColor):
                stream('q')
                streamStroke = self.streamStrokeColor(borderColor)
                hbw = borderWidth*0.5
                ds = size - borderWidth
                if borderStyle=='underlined':
                    stream('q %(streamStroke)s %(borderWidth)s w 0 %(hbw)s m %(size)s %(hbw)s l s Q')
                elif borderStyle in ('dashed','inset','bevelled','solid'):
                    if borderStyle=='dashed':
                        dash = ' [3 ] 0 d'
                    else:
                        dash = ''
                    stream('%(streamStroke)s%(dash)s %(borderWidth)s w')
                    stream(cas(r-hbw))
                    stream('s')
                stream('Q')
                if borderStyle in ('bevelled','inset') and opaqueColor(fillColor):
                    _3bwh = 3*hbw
                    ds = size - _3bwh
                    bbs0 = Blacker(fillColor,0.5)
                    bbs1 = Whiter(fillColor,0.5)
                    a0 = (0,1)
                    a1 = (2,3)
                    if borderStyle=='inset':
                        bbs0, bbs1 = bbs1, bbs0
                    if key!='D':
                        bbs0, bbs1 = bbs1, bbs0
                    bbs0 = self.streamStrokeColor(bbs0)
                    bbs1 = self.streamStrokeColor(bbs1)
                    stream('q %(bbs0)s %(borderWidth)s w')
                    stream(cas(r-_3bwh,rotated=True,arcs=a0))
                    stream('S Q %(bbs1)s q')
                    stream(cas(r-_3bwh,rotated=True,arcs=a1))
                    stream('S Q')
        if value=='Yes':
            if opaqueColor(textColor):
                textFillColor = self.streamFillColor(textColor)
                textStrokeColor = self.streamStrokeColor(textColor)
            else:
                textFillColor = '0 g'
                textStrokeColor = '0 G'
            stream('q %(textFillColor)s %(textStrokeColor)s')
            cbm = cbmarks[buttonStyle]
            if shape=='circle' and buttonStyle=='circle':
                stream(cas((max(r-(size-ds),1))*0.5))
                stream('f')
            else:
                stream(cbm.scaledRender(size,size-ds))
            stream('Q')
        stream = ('\n'.join(stream.__self__) % vars()).replace('  ',' ').replace('\n\n','\n')
        return self.makeStream(
                size, size, stream,
                Resources = PDFFromString('<< /ProcSet [/PDF] >>'),
                )

    @staticmethod
    def circleArcStream(size, r, arcs=(0,1,2,3), rotated=False):
        R = [].append
        rlen = R.__self__.__len__
        hsize = size * 0.5
        f = size / 20.0
        size *= f 
        hsize *= f 
        r *= f
        cp = fp_str(0.55231 * r)
        r = fp_str(r)
        hsize = fp_str(hsize)
        mx = '0.7071 0.7071 -0.7071 0.7071' if rotated else '1 0 0 1'
        R('%(mx)s %(hsize)s %(hsize)s cm')
        if 0 in arcs:
            if rlen()==1: R('%(r)s 0 m')
            R('%(r)s %(cp)s %(cp)s %(r)s 0 %(r)s c')
        if 1 in arcs:
            if rlen()==1: R('0 %(r)s m')
            R('-%(cp)s %(r)s -%(r)s %(cp)s -%(r)s 0 c')
        if 2 in arcs:
            if rlen()==1: R('-%(r)s 0 m')
            R('-%(r)s -%(cp)s -%(cp)s -%(r)s 0 -%(r)s c')
        if 3 in arcs:
            if rlen()==1: R('0 -%(r)s m')
            R('%(cp)s -%(r)s %(r)s -%(cp)s %(r)s 0 c')
        return '\n'.join(R.__self__) % vars()

    def zdMark(self,c,size,ds,iFontName):
        c = ZDSyms[c]
        W = H = size-ds
        fs = H/1.2
        w = float(stringWidth(c,'ZapfDingbats',fs))
        if w>W:
            fs *= W/w
        dx = ds + 0.5*(W-w)
        dy = 0
        return 'BT %(iFontName)s %(fs)s Tf %(dx)s %(dy)s Td %(fs)s TL (%(c)s) Tj ET' % vars()


    def getRef(self,obj):
        return self.canv._doc.Reference(obj)

    def getRefStr(self,obj):
        return asNative(self.getRef(obj).format(self.canv._doc))

    _defaultValues = dict(
                        cmyk_textColor=CMYKColor(0,0,0,0.9),
                        cmyk_borderColor=CMYKColor(0,0,0,0.9),
                        cmyk_fillColor=CMYKColor(0.12,0.157,0,0),
                        rgb_textColor=Color(0.1,0.1,0.1),
                        rgb_borderColor=Color(0.1,0.1,0.1),
                        rgb_fillColor=Color(0.8,0.843,1),
                        borderWidth=1,
                        )

    def setDefault(self, name, value):
        if '_defaultValues' not in self.__dict__:
            self._defaultValues = self.__class__._defaultValues.clone()
        
        d = self._defaultValues()
        if name in d.keys():
            if '_' in name:
                #its a colour name
                if name[:4]=='cmyk':
                    if isinstance(value,(CMYKColor,NoneType)):
                        d[name] = value
                        return
                elif isinstance(value,(Color,NoneType)):
                        d[name] = value
                        return
            elif isinstance(value(float,int,NoneType)):
                #it's a number
                d[name] = value
                return
            raise ValueError(f'Invalid value in acroForm.setDefault({name!r},{value!r})')
        raise ValueError(f'Invalid name in acroForm.setDefault({name!r},{value!r})')
                    
    def getDefaults(self,textColor,borderColor,fillColor, borderWidth):
        pfx = (isinstance(fillColor,CMYKColor)
            or isinstance(textColor,CMYKColor)
            or isinstance(borderColor,CMYKColor))
        if not pfx: pfx = getattr(self.canv._enforceColorSpace,'__name__','_enforceRGB')!='_enforceRGB'
            
        pfx = 'cmyk_' if pfx else 'rgb_'
        d = self._defaultValues
        return (textColor if textColor is not _useDefault else d[pfx+'textColor'],
                borderColor if borderColor is not _useDefault else d[pfx+'borderColor'],
                fillColor if fillColor is not _useDefault else d[pfx+'fillColor'],
                borderWidth if borderWidth is not _useDefault else d['borderWidth'],
                )
    
    @staticmethod
    def varyColors(key,t,b,f):
        if key!='N':
            func = Whiter if key=='R' else Blacker
            t,b,f = [func(c,0.9) if c is not None else None for c in (t,b,f)]
        return t,b,f

    def checkForceBorder(self,x,y,width,height,forceBorder,shape,borderStyle,borderWidth,borderColor,fillColor):
        if forceBorder:
            canv = self.canv
            canv.saveState()
            canv.resetTransforms()
            if borderWidth is not None and opaqueColor(borderColor):
                hbw = 0.5*borderWidth
                canv.setLineWidth(borderWidth)
                canv.setStrokeColor(borderColor)
                s = 1
            else:
                s = hbw = 0
            width -= 2*hbw
            height -= 2*hbw
            x += hbw
            y += hbw
            doFill = opaqueColor(fillColor)
            if doFill:
                canv.setFillColor(fillColor)
            if shape=='square':
                canv.rect(x,y,width,height,stroke=s,fill=1 if doFill else 0)
            else:
                r = min(width,height) * 0.5
                canv.circle(x+r,y+r,r,stroke=s,fill=1 if doFill else 0)
            canv.restoreState()

    def checkbox(self,
                checked=False,
                buttonStyle='check',
                shape='square',
                fillColor=_useDefault,
                borderColor=_useDefault,
                textColor=_useDefault,
                borderWidth=_useDefault,
                borderStyle='solid',
                size=20,
                x=0,
                y=0,
                tooltip=None,
                name=None,
                annotationFlags='print',
                fieldFlags='required',
                forceBorder=False,
                relative=False,
                dashLen = 3,
                ):
        initialValue = 'Yes' if checked else 'Off'
        textColor,borderColor,fillColor,borderWidth = self.getDefaults(textColor,borderColor,fillColor,borderWidth)
        canv = self.canv
        if relative:
            x, y = self.canv.absolutePosition(x,y)
        doc = canv._doc
        AP = {}
        for key in 'NDR':
            APV = {}
            tC,bC,fC = self.varyColors(key,textColor,borderColor,fillColor)
            for value in ('Yes','Off'):
                ap = self.checkboxAP(
                                    key,
                                    value,
                                    buttonStyle=buttonStyle,
                                    shape=shape,
                                    fillColor=fC,
                                    borderColor=bC,
                                    textColor=tC,
                                    borderWidth=borderWidth,
                                    borderStyle=borderStyle,
                                    size=size,
                                    dashLen=dashLen,
                                    )
                if ap._af_refstr in self._refMap:
                    ref = self._refMap[ap._af_refstr]
                else:
                    ref = self.getRef(ap)
                    self._refMap[ap._af_refstr] = ref
                APV[value]=ref
            AP[key] = PDFDictionary(APV)
            del APV
        CB = dict(
                FT = PDFName('Btn'),
                P = doc.thisPageRef(),
                V = PDFName(initialValue),
                AS = PDFName(initialValue),
                #DV = PDFName(initialValue),
                Rect = PDFArray((x,y,x+size,y+size)),
                AP = PDFDictionary(AP),
                Subtype = PDFName('Widget'),
                Type = PDFName('Annot'),
                F = makeFlags(annotationFlags,annotationFlagValues),
                Ff = makeFlags(fieldFlags,fieldFlagValues),
                H=PDFName('N'),
                )
        if tooltip:
            CB['TU'] = PDFString(tooltip)
        if not name:
            name = 'AFF%03d' % len(self.fields)
        if borderWidth: CB['BS'] = bsPDF(borderWidth,borderStyle,dashLen)
        CB['T'] = PDFString(name)
        MK = dict(CA='(%s)' % ZDSyms[buttonStyle])
        if borderColor is not None:
            MK['BC'] = PDFArray(self.colorTuple(borderColor))
        if fillColor is not None:
            MK['BG'] = PDFArray(self.colorTuple(fillColor))
        CB['MK'] = PDFDictionary(MK)
        CB = PDFDictionary(CB)
        self.canv._addAnnotation(CB)
        self.fields.append(self.getRef(CB))
        self.checkForceBorder(x,y,size,size,forceBorder,shape,borderStyle,borderWidth,borderColor,fillColor)

    def radio(self,
                value=None,
                selected=False,
                buttonStyle='circle',
                shape='circle',
                fillColor=_useDefault,
                borderColor=_useDefault,
                textColor=_useDefault,
                borderWidth=_useDefault,
                borderStyle='solid',
                size=20,
                x=0,
                y=0,
                tooltip=None,
                name=None,
                annotationFlags='print',
                fieldFlags='noToggleToOff required radio',
                forceBorder=False,
                relative=False,
                dashLen=3,
                ):
        if name not in self._radios:
            group = RadioGroup(name,tooltip=tooltip,fieldFlags=fieldFlags)
            group._ref = self.getRef(group)
            self._radios[name] = group
            self.fields.append(group._ref)
        else:
            group = self._radios[name]
            fieldFlags = makeFlags(fieldFlags,fieldFlagValues)
            if fieldFlags!=group.Ff:
                raise ValueError('radio.%s.%s created with different flags' % (name,value))
        if not value:
            raise ValueError('bad value %r for radio.%s' % (value,name))
        initialValue = value if selected else 'Off'
        textColor,borderColor,fillColor,borderWidth = self.getDefaults(textColor,borderColor,fillColor,borderWidth)

        if initialValue==value:
            if group.V is not None:
                if group.V!=value:
                    raise ValueError('radio.%s.%s sets initial value conflicting with %s'%(name,value,group.V))
            else:
                group.V = value
        canv = self.canv
        if relative:
            x, y = self.canv.absolutePosition(x,y)
        doc = canv._doc
        AP = {}
        for key in 'NDR':
            APV = {}
            tC,bC,fC = self.varyColors(key,textColor,borderColor,fillColor)
            for v in (value,'Off'):
                ap = self.checkboxAP(
                                    key,
                                    'Yes' if v==value else 'Off',
                                    buttonStyle=buttonStyle,
                                    shape=shape,
                                    fillColor=fC,
                                    borderColor=bC,
                                    textColor=tC,
                                    borderWidth=borderWidth,
                                    borderStyle=borderStyle,
                                    size=size,
                                    dashLen=dashLen,
                                    )
                if ap._af_refstr in self._refMap:
                    ref = self._refMap[ap._af_refstr]
                else:
                    ref = self.getRef(ap)
                    self._refMap[ap._af_refstr] = ref
                APV[v]=ref
            AP[key] = PDFDictionary(APV)
            del APV
        RB = dict(
                FT = PDFName('Btn'),
                P = doc.thisPageRef(),
                AS = PDFName(initialValue),
                #DV = PDFName(initialValue),
                Rect = PDFArray((x,y,x+size,y+size)),
                AP = PDFDictionary(AP),
                Subtype = PDFName('Widget'),
                Type = PDFName('Annot'),
                F = makeFlags(annotationFlags,annotationFlagValues),
                Parent = group._ref,
                #DA = PDFString('1 g '+(self.streamFillColor(fillColor) if fillColor else '-0.25 0.75 -0.25 rg'))
                H=PDFName('N'),
                )
        #RB['T'] = PDFString(name)
        MK = dict(CA='(%s)' % ZDSyms[buttonStyle])
        if borderColor is not None:
            MK['BC'] = PDFArray(self.colorTuple(borderColor))
        if fillColor is not None:
            MK['BG'] = PDFArray(self.colorTuple(fillColor))
        if borderWidth: RB['BS'] = bsPDF(borderWidth,borderStyle,dashLen)
        RB['MK'] = PDFDictionary(MK)
        RB = PDFDictionary(RB)
        self.canv._addAnnotation(RB)
        group.kids.append(self.getRef(RB))
        self.checkForceBorder(x,y,size,size,forceBorder,shape,borderStyle,borderWidth,borderColor,fillColor)

    def makeStream(self,
                width,
                height,
                stream,
                **D
                ):
        D['Matrix'] = PDFArray([1.0,0.0,0.0,1.0,0.0,0.0])
        D['BBox'] = PDFArray([0,0,width,height])
        D['Subtype'] = PDFName('Form')
        D['Type'] = PDFName('XObject')
        D['FormType'] = 1

        s = PDFStream(
                PDFDictionary(D),
                stream,
                filters = [PDFStreamFilterZCompress()] if self.canv._doc.compression else None,
                )
        #compute a lookup string
        s._af_refstr = stream+'\n'.join(('%s=%r' % (k,_pdfObjToStr(v)) for k,v in sorted(D.items())))
        return s

    def txAP(self,
                key,                    #N/D/R
                value,
                iFontName,
                rFontName,
                fontSize,
                shape='square',
                fillColor=None,
                borderColor=None,
                textColor=None,
                borderWidth=None,
                borderStyle='solid',
                width=120,
                height=36,
                dashLen=3,
                wkind='textfield',
                labels=[],
                I=[],
                sel_bg='0.600006 0.756866 0.854904 rg',
                sel_fg='0 g',
                ):
        _stream = []
        stream = _stream.append
        if opaqueColor(fillColor):
            streamFill = self.streamFillColor(fillColor)
            stream('%(streamFill)s\n0 0 %(width)s %(height)s re\nf')
        # Pre-initialise border-derived variables so they are always defined for
        # the /Tx clipping-region stream that follows, even when no border is drawn.
        hbw = _2bw = 0
        bww = width
        bwh = height
        undash = ''
        if borderWidth!=None and borderWidth>0 and opaqueColor(borderColor):
            hbw = borderWidth*0.5
            bww = width - borderWidth
            bwh = height - borderWidth
            _2bw = 2*borderWidth
            if borderStyle in ('bevelled','inset'):
                bw2w = width - _2bw
                bw2h = height - _2bw
                if borderStyle == 'bevelled':
                    bbs0 = '1 g'
                    if fillColor or borderColor:
                        bbs1 = '-0.250977 0.749023 -0.250977 rg'
                    else:
                        bbs1 = '.75293 g'
                else:
                    bbs0 = '.501953 g'
                    bbs1 = '.75293 g'
                stream('%(bbs0)s\n%(borderWidth)s %(borderWidth)s m\n%(borderWidth)s %(bwh)s l\n%(bww)s %(bwh)s l\n%(bw2w)s %(bw2h)s l\n%(_2bw)s %(bw2h)s l\n%(_2bw)s %(_2bw)s l\nf\n%(bbs1)s\n%(bww)s %(bwh)s m\n%(bww)s %(borderWidth)s l\n%(borderWidth)s %(borderWidth)s l\n%(_2bw)s %(_2bw)s l\n%(bw2w)s %(_2bw)s l\n%(bw2w)s %(bw2h)s l\nf')
        else:
            hbw = _2bw = borderWidth = 0
            bww = width
            bwh = height
        undash = ''
        if opaqueColor(borderColor) and borderWidth:
            streamStroke = self.streamStrokeColor(borderColor)
            if borderStyle=='underlined':
                stream('%(streamStroke)s %(borderWidth)s w 0 %(hbw)s m %(width)s %(hbw)s l s')
            elif borderStyle in ('dashed','inset','bevelled','solid'):
                if borderStyle=='dashed':
                    dash = '\n[%s ] 0 d\n' % fp_str(dashLen)
                    undash = '[] 0 d'
                else:
                    dash = '\n%s w' % borderWidth
                stream('%(streamStroke)s\n%(dash)s\n%(hbw)s %(hbw)s %(bww)s %(bwh)s re\ns')
        _4bw = 4*borderWidth
        w4bw = width - _4bw
        h4bw = height - _4bw
        textFill = self.streamFillColor(textColor) if opaqueColor(textColor) else '0 g'
        stream('/Tx BMC \nq\n%(_2bw)s %(_2bw)s %(w4bw)s %(h4bw)s re\nW\nn')
        leading = 1.2 * fontSize
        if wkind=='listbox':
            nopts = int(h4bw/leading)
            leading = h4bw/float(nopts)
            if nopts>len(labels):
                i0 = 0
                nopts = len(labels)
            elif len(I)<=1:
                i0 = I[0] if I else 0
                if i0:
                    if i0<nopts:
                        i0 = 0
                    else:
                        i = len(labels) - nopts
                        if i0>=i:
                            i0 = i
            else:   #|I|>1
                if I[1]<nopts:
                    i0 = 0
                else:
                    i0 = I[0]
            y = len(labels)
            i = i0 + nopts
            if i>y: i0 = i - y
            ilim = min(y,i0+nopts)
            if I:
                i = i0
                y = height - _2bw - leading
                stream(sel_bg)
                while i<ilim:
                    if i in I:
                        #draw selected bg
                        stream('%%(_2bw)s %s %%(w4bw)s %%(leading)s re\nf' % fp_str(y))
                    y -= leading
                    i += 1
            i = i0
            y = height - _2bw - fontSize
            stream('0 g\n0 G\n%(undash)s')
            while i<ilim:
                stream('BT')
                if i==i0:
                    stream('/%(iFontName)s %(fontSize)s Tf')
                stream(sel_fg if i in I else '%(textFill)s')
                stream('%%(_4bw)s %s Td\n(%s) Tj' % (fp_str(y),escPDF(labels[i])))
                y -= leading
                i += 1
                stream('ET')
        else:
            stream('0 g\n0 G\n%(undash)s')
            if value:
                stream('BT\n/%(iFontName)s %(fontSize)s Tf\n%(textFill)s')
                stream('1 0 0 1 %%(_4bw)s %s Tm' % fp_str(height - fontSize - _2bw)) 
                for line in value.split('\n'):
                    stream('(%s) Tj\n0 %s Td' % (escPDF(line),fp_str(-leading)))
                #the last change is not needed
                _stream[-1] = _stream[-1][:_stream[-1].rfind('\n')]
                stream('ET')
        leading = fp_str(leading)
        stream('Q\nEMC\n')
        stream = ('\n'.join(_stream) % vars()).replace('  ',' ').replace('\n\n','\n')
        return self.makeStream(
                width, height, stream,
                Resources = PDFFromString('<< /ProcSet [/PDF /Text] /Font %(rFontName)s >>' % vars()),
                )

    def makeFont(self,fontName):
        if fontName is None:
            fontName = 'Helvetica'
        if fontName not in self.formFontNames:
            raise ValueError('form font name, %r, is not one of the standard 14 fonts' % fontName)
        fn = self.formFontNames[fontName]
        ref = self.getRefStr(PDFFromString('<< /BaseFont /%s /Subtype /Type1 /Name /%s /Type /Font /Encoding %s >>' % (
                        fontName,fn,self.encRefStr)))
        if fn not in self.fonts:
            self.fonts[fn] = ref
        return ref, fn

    def _textfield(self,
                value='',
                fillColor=_useDefault,
                borderColor=_useDefault,
                textColor=_useDefault,
                borderWidth=_useDefault,
                borderStyle='solid',
                width=120,
                height=36,
                x=0,
                y=0,
                tooltip=None,
                name=None,
                annotationFlags='print',
                fieldFlags='',
                forceBorder=False,
                relative=False,
                maxlen=100,
                fontName=None,
                fontSize=None,
                wkind=None,
                options=None,
                dashLen=3,
                ):
        rFontName, iFontName = self.makeFont(fontName)
        # Decide which widget dictionary entries to write before fontSize gets
        # its rendering default.  Omitting /DA, /MK /BG, and /MK /BC when they
        # were not explicitly requested lets the field inherit appearance from
        # the AcroForm-level /DA//DR or the PDF viewer's built-in default.
        _write_da = textColor is not None or fontName is not None or fontSize is not None
        if fontSize is None:
            fontSize = 12
        textColor,borderColor,fillColor,borderWidth = self.getDefaults(textColor,borderColor,fillColor,borderWidth)
        canv = self.canv
        if relative:
            x, y = self.canv.absolutePosition(x,y)
        doc = canv._doc
        rFontName = '<</%s %s>>' % (iFontName,rFontName)
        Ff = makeFlags(fieldFlags,fieldFlagValues)
        if wkind!='textfield':
            #options must be a list of pairs (label value)
            #value must be a list of the values
            FT='Ch'
            if wkind=='choice':
                Ff |= fieldFlagValues['combo']  #just in case
            V = []
            Opt = []
            AP = []
            I = []
            TF = []
            if not isinstance(options,(list,tuple)):
                raise TypeError('%s options=%r is wrong type' % (wkind,options))
            for v in options:
                if isStr(v):
                    Opt.append(PDFString(v))
                    l = v
                elif isinstance(v,(list,tuple)):
                    if len(v)==1:
                        v=l=v[0]
                    else:
                        l,v = v
                    Opt.append(PDFArray([PDFString(v),PDFString(l)]))
                else:
                    raise TypeError('%s option %r is wrong type' % (wkind,v))
                AP.append(v)
                TF.append(l)
            Opt = PDFArray(Opt)
            if value:
                if not isinstance(value,(list,tuple)):
                    value = [value]
                for v in value:
                    if v not in AP:
                        if v not in TF:
                            raise ValueError('%s value %r is not in option\nvalues %r\nor labels %r' % (wkind,v,AP,TF))
                        else:
                            v = AP[TF.index(v)]
                    I.append(AP.index(v))
                    V.append(PDFString(v))
                I.sort()
                if not (Ff & fieldFlagValues['multiSelect']) or len(value)==1:
                    if wkind=='choice':
                        value = TF[I[0]]
                    else:
                        value = value[:1]
                    V = V[:1]
                V = V[0] if len(V)==1 else PDFArray(V)
                lbextras = dict(labels=TF,I=I,wkind=wkind)
            else:
                V = PDFString(value)
        else:
            I = Opt = []
            lbextras = {}
            FT='Tx'
            if not isStr(value):
                raise TypeError('textfield value=%r is wrong type' % value)
            V = PDFString(value)
        AP = {}
        for key in 'N':
            tC,bC,fC = self.varyColors(key,textColor,borderColor,fillColor)
            ap = self.txAP(
                            key,
                            value,
                            iFontName,
                            rFontName,
                            fontSize,
                            fillColor=fC,
                            borderColor=bC,
                            textColor=tC,
                            borderWidth=borderWidth,
                            borderStyle=borderStyle,
                            width=width,
                            height=height,
                            dashLen = dashLen,
                            **lbextras
                            )
            if ap._af_refstr in self._refMap:
                ref = self._refMap[ap._af_refstr]
            else:
                ref = self.getRef(ap)
                self._refMap[ap._af_refstr] = ref
            AP[key] = ref

        TF = dict(
                FT = PDFName(FT),
                P = doc.thisPageRef(),
                V = V,
                #AS = PDFName(value),
                DV = V,
                Rect = PDFArray((x,y,x+width,y+height)),
                AP = PDFDictionary(AP),
                Subtype = PDFName('Widget'),
                Type = PDFName('Annot'),
                F = makeFlags(annotationFlags,annotationFlagValues),
                Ff = Ff,
                #H=PDFName('N'),
                )
        # Write /DA only when font or text color was explicitly requested.
        # Omitting /DA lets the widget inherit its default appearance from the
        # AcroForm /DA string instead of overriding it with a widget-level entry.
        if _write_da:
            da_color = self.streamFillColor(textColor) if opaqueColor(textColor) else '0 g'
            TF['DA'] = PDFString('/%s %d Tf %s' % (iFontName,fontSize, da_color))
        if Opt: TF['Opt'] = Opt
        if I: TF['I'] = PDFArray(I)
        if maxlen:
            TF['MaxLen'] = maxlen
        if tooltip:
            TF['TU'] = PDFString(tooltip)
        if not name:
            name = 'AFF%03d' % len(self.fields)
        TF['T'] = PDFString(name)
        MK = {}
        if fillColor is not None:
            MK['BG'] = PDFArray(self.colorTuple(fillColor))
        # Acrobat seems to draw a thin border when BS is defined, so only
        # include this if there actually is a border to draw
        if borderWidth:
            TF['BS'] = bsPDF(borderWidth,borderStyle,dashLen)
            if borderColor is not None:
                MK['BC'] = PDFArray(self.colorTuple(borderColor))
        if MK:
            TF['MK'] = PDFDictionary(MK)

        TF = PDFDictionary(TF)
        self.canv._addAnnotation(TF)
        self.fields.append(self.getRef(TF))
        self.checkForceBorder(x,y,width,height,forceBorder,'square',borderStyle,borderWidth,borderColor,fillColor)

    def textfield(self,
                value='',
                fillColor=_useDefault,
                borderColor=_useDefault,
                textColor=_useDefault,
                borderWidth=_useDefault,
                borderStyle='solid',
                width=120,
                height=36,
                x=0,
                y=0,
                tooltip=None,
                name=None,
                annotationFlags='print',
                fieldFlags='',
                forceBorder=False,
                relative=False,
                maxlen=100,
                fontName=None,
                fontSize=None,
                dashLen=3,
                ):
        return self._textfield(
                value=value,
                fillColor=fillColor,
                borderColor=borderColor,
                textColor=textColor,
                borderWidth=borderWidth,
                borderStyle=borderStyle,
                width=width,
                height=height,
                x=x,
                y=y,
                tooltip=tooltip,
                name=name,
                annotationFlags=annotationFlags,
                fieldFlags=fieldFlags,
                forceBorder=forceBorder,
                relative=relative,
                maxlen=maxlen,
                fontName=fontName,
                fontSize=fontSize,
                dashLen=dashLen,
                wkind='textfield',
                )

    def listbox(self,
                value='',
                fillColor=_useDefault,
                borderColor=_useDefault,
                textColor=_useDefault,
                borderWidth=_useDefault,
                borderStyle='solid',
                width=120,
                height=36,
                x=0,
                y=0,
                tooltip=None,
                name=None,
                annotationFlags='print',
                fieldFlags='',
                forceBorder=False,
                relative=False,
                fontName=None,
                fontSize=None,
                dashLen=3,
                maxlen=None,
                options=[],
                ):
        return self._textfield(
                value=value,
                fillColor=fillColor,
                borderColor=borderColor,
                textColor=textColor,
                borderWidth=borderWidth,
                borderStyle=borderStyle,
                width=width,
                height=height,
                x=x,
                y=y,
                tooltip=tooltip,
                name=name,
                annotationFlags=annotationFlags,
                fieldFlags=fieldFlags,
                forceBorder=forceBorder,
                relative=relative,
                maxlen=maxlen,
                fontName=fontName,
                fontSize=fontSize,
                dashLen=dashLen,
                wkind='listbox',
                options = options,
                )
    def choice(self,
                value='',
                fillColor=_useDefault,
                borderColor=_useDefault,
                textColor=_useDefault,
                borderWidth=_useDefault,
                borderStyle='solid',
                width=120,
                height=36,
                x=0,
                y=0,
                tooltip=None,
                name=None,
                annotationFlags='print',
                fieldFlags='combo',
                forceBorder=False,
                relative=False,
                fontName=None,
                fontSize=None,
                dashLen=3,
                maxlen=None,
                options=[],
                ):
        return self._textfield(
                value=value,
                fillColor=fillColor,
                borderColor=borderColor,
                textColor=textColor,
                borderWidth=borderWidth,
                borderStyle=borderStyle,
                width=width,
                height=height,
                x=x,
                y=y,
                tooltip=tooltip,
                name=name,
                annotationFlags=annotationFlags,
                fieldFlags=fieldFlags,
                forceBorder=forceBorder,
                relative=relative,
                maxlen=maxlen,
                fontName=fontName,
                fontSize=fontSize,
                dashLen=dashLen,
                wkind='choice',
                options = options,
                )

    def checkboxRelative(self, **kwds):
        "same as checkbox except the x and y are relative to the canvas coordinate transform"
        kwds['relative']=True
        self.checkbox(**kwds)

    def radioRelative(self, **kwds):
        "same as radio except the x and y are relative to the canvas coordinate transform"
        kwds['relative']=True
        self.radio(**kwds)

    def textfieldRelative(self, **kwds):
        "same as textfield except the x and y are relative to the canvas coordinate transform"
        kwds['relative']=True
        self.textfield(**kwds)

    def listboxRelative(self, **kwds):
        "same as textfield except the x and y are relative to the canvas coordinate transform"
        kwds['relative']=True
        self.textfield(**kwds)
    def choiceRelative(self, **kwds):
        "same as textfield except the x and y are relative to the canvas coordinate transform"
        kwds['relative']=True
        self.textfield(**kwds)

    @property
    def encRefStr(self):
        if not self._pdfdocenc:
            self._pdfdocenc = PDFFromString('''<</Type /Encoding /Differences [24 /breve /caron /circumflex /dotaccent /hungarumlaut /ogonek /ring /tilde 39 /quotesingle 96 /grave 128 /bullet /dagger /daggerdbl /ellipsis /emdash /endash /florin /fraction /guilsinglleft /guilsinglright /minus /perthousand /quotedblbase /quotedblleft /quotedblright /quoteleft /quoteright /quotesinglbase /trademark /fi /fl /Lslash /OE /Scaron /Ydieresis /Zcaron /dotlessi /lslash /oe /scaron /zcaron 160 /Euro 164 /currency 166 /brokenbar 168 /dieresis /copyright /ordfeminine 172 /logicalnot /.notdef /registered /macron /degree /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]>>''')
        return self.getRefStr(self._pdfdocenc)

class CBMark:
    opNames = 'm l c h'.split()
    opCount = 1,1,3,0

    def __init__(self,ops,points,bounds,slack=0.05):
        self.ops = ops
        self.xmin,self.ymin,self.xmax,self.ymax = bounds
        self.points = points
        self.slack = slack

    def scaledRender(self,size,ds=0):
        '''
        >>> print(cbmarks['check'].scaledRender(20))
        12.97075 14.68802 m 15.00139 17.16992 l 15.9039 18.1727 17.93454 18.67409 19.2883 18.67409 c 19.46379 18.27298 l 17.13231 15.51532 l 11.91783 8.62117 l 8.307799 3.030641 l 7.430362 1.526462 l 7.305014 1.275766 7.154596 .97493 6.9039 .824513 c 6.577994 .674095 5.825905 .674095 5.47493 .674095 c 4.672702 .674095 4.497214 .674095 4.321727 .799443 c 4.071031 .97493 3.945682 1.325905 3.770195 1.67688 c 3.218663 2.830084 2.240947 5.337047 2.240947 6.590529 c 2.240947 7.016713 2.491643 7.21727 2.817549 7.442897 c 3.344011 7.818942 4.0961 8.245125 4.747911 8.245125 c 5.249304 8.245125 5.299443 7.818942 5.449861 7.417827 c 5.951253 6.239554 l 6.026462 6.038997 6.252089 5.337047 6.527855 5.337047 c 6.778552 5.337047 7.079387 5.913649 7.179666 6.089136 c 12.97075 14.68802 l h f
        >>> print(cbmarks['cross'].scaledRender(20))
        19.9104 17.43931 m 12.41908 10 l 19.9104 2.534682 l 18.37572 1 l 10.9104 8.491329 l 3.445087 1 l 1.910405 2.534682 l 9.427746 10 l 1.910405 17.46532 l 3.445087 19 l 10.9104 11.50867 l 18.37572 19 l 19.9104 17.43931 l h f
        >>> print(cbmarks['circle'].scaledRender(20))
        1.872576 9.663435 m 1.872576 14.64958 5.936288 18.61357 10.89751 18.61357 c 15.8338 18.61357 19.87258 14.59972 19.87258 9.663435 c 19.87258 4.727147 15.8338 .688366 10.89751 .688366 c 5.936288 .688366 1.872576 4.677285 1.872576 9.663435 c h f
        >>> print(cbmarks['star'].scaledRender(20))
        10.85542 18.3253 m 12.90361 11.84337 l 19.84337 11.84337 l 14.25301 7.650602 l 16.42169 1 l 10.85542 5.096386 l 5.289157 1 l 7.481928 7.650602 l 1.843373 11.84337 l 8.759036 11.84337 l 10.85542 18.3253 l h f
        >>> print(cbmarks['diamond'].scaledRender(20))
        17.43533 9.662031 m 15.63282 7.484006 l 10.85118 .649513 l 8.422809 4.329624 l 5.919332 7.659249 l 4.267038 9.662031 l 6.16968 12.0153 l 10.85118 18.64951 l 12.75382 15.4701 15.00695 12.49096 17.43533 9.662031 c h f
        '''
        #work out the scale and translation
        W = H = size - 2*ds
        xmin = self.xmin
        ymin = self.ymin
        w = self.xmax-xmin
        h = self.ymax-ymin
        slack = self.slack*min(W,H)
        sx = (W - 2*slack)/float(w)
        sy = (H - 2*slack)/float(h)
        sx = sy = min(sx,sy)
        w *= sx
        h *= sy
        dx = ds+(W - w)*0.5
        dy = ds+(H - h)*0.5
        xsc = lambda v: fp_str((v-xmin)*sx+dx)
        ysc = lambda v: fp_str((v-ymin)*sy+dy)

        opNames = self.opNames
        opCount = self.opCount
        C = [].append
        i = 0
        points = self.points
        for op in self.ops:
            c = opCount[op]
            for _ in range(c):
                C(xsc(points[i]))
                C(ysc(points[i+1]))
                i += 2
            C(opNames[op])
        C('f')
        return ' '.join(C.__self__)

cbmarks = dict(
        check=CBMark(
                    [0, 1, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 1, 3],
                    [462, 546, 543, 645, 579, 685, 660, 705, 714, 705, 721, 689, 628, 579, 420, 304, 276, 81, 241, 21, 236, 11, 230, -1, 220, -7, 207, -13, 177, -13, 163, -13, 131, -13, 124, -13, 117, -8, 107, -1, 102, 13, 95, 27, 73, 73, 34, 173, 34, 223, 34, 240, 44, 248, 57, 257, 78, 272, 108, 289, 134, 289, 154, 289, 156, 272, 162, 256, 182, 209, 185, 201, 194, 173, 205, 173, 215, 173, 227, 196, 231, 203, 462, 546],
                    (34,-12,721,706),
                    ),
        cross = CBMark(
                    [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3],
                    [727, 632, 439, 346, 727, 59, 668, 0, 381, 288, 94, 0, 35, 59, 324, 346, 35, 633, 94, 692, 381, 404, 668, 692, 727, 632],
                    (35,0,727,692),
                    ),
        circle = CBMark(
                    [0, 2, 2, 2, 2, 3],
                    [35, 346, 35, 546, 198, 705, 397, 705, 595, 705, 757, 544, 757, 346, 757, 148, 595, -14, 397, -14, 198, -14, 35, 146, 35, 346],
                    (35,-14,757,705),
                    ),
        star = CBMark(
                    [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3],
                    [409, 705, 494, 436, 782, 436, 550, 262, 640, -14, 409, 156, 178, -14, 269, 262, 35, 436, 322, 436, 409, 705],
                    (35,-14,782,705),
                    ),
        diamond = CBMark(
                    [0, 1, 1, 1, 1, 1, 1, 1, 2, 3],
                    [560, 346, 488, 259, 297, -14, 200, 133, 100, 266, 34, 346, 110, 440, 297, 705, 373, 578, 463, 459, 560, 346],
                    (34,-14,560,705),
                    ),
        )
ZDSyms=dict(check='4',cross='5',circle='l',star='N',diamond='u')

if __name__ == "__main__":
    import doctest
    doctest.testmod()
