#pageflow.py
"""Support for using PageCatcher within Platypus"""
__all__ = ('LoadPdfFlowable', 'ShowPdfFlowable', 'loadPdf',
            )
import os, pickle
from reportlab.platypus.flowables import Flowable, PageBreak
from rlextra.pageCatcher.pageCatcher import storeFormsInMemory,\
        restoreFormsInMemory, restoreFormsFromDict, fileName2Prefix
from reportlab.pdfbase.pdfdoc import xObjectName
from reportlab.platypus.doctemplate import NextPageTemplate, NotAtTopPageBreak
from reportlab.lib.utils import open_and_read, asUnicodeEx

class LoadPdfFlowable(Flowable):
    """Imports PDF content into the current canvas when drawn.

    Used by RML2PDF, which has to store the forms itself but defer
    placing them into a document"""
    def __init__(self, pickledStuff, isDict=False, iptrans=None):
        Flowable.__init__(self)
        self.pickledStuff = pickledStuff
        self._isDict = isDict
        self._iptrans = iptrans

    def drawOn(self, canvas, x, y, _sW=0):
        "This makes sure the form XObjects are loaded into the canvas"
        iptrans = self._iptrans
        dor = iptrans and not iptrans.trivial()
        if dor:
            canvas.saveState()
            if not iptrans.noRotate():
                canvas.rotate(iptrans.degrees)
            if not iptrans.noTranslate():
                canvas.translate(iptrans.dx,iptrans.dy)
            if not iptrans.noScale():
                canvas.scale(iptrans.sx,iptrans.sy)
        (self._isDict and restoreFormsFromDict or restoreFormsInMemory)(self.pickledStuff,
                             canvas,
                             allowDuplicates=1)
        if dor:
            canvas.restoreState()

def _formName2ExtraInfo(canv,name):
    doc = canv._doc
    internalname = xObjectName(name)
    if internalname in doc.idToObject:
        form = doc.idToObject[internalname]
        if hasattr(form,'_extra_pageCatcher_info'):
            return form._extra_pageCatcher_info

def _formName2OrientationMatrix(canv,name,orientation):
    xi = _formName2ExtraInfo(canv,name)
    if xi:
        po = xi.get('Rotate',0)
        x0,y0,x1,y1 = xi['MediaBox']
        if orientation=='auto':
            orientation=po
        orientation = int(orientation % 360)
        if orientation==0:
            if not x0 and not y0: return
            matrix = [1,0,0,1,-x0,-y0]
        elif orientation==90:
            matrix = [0,-1,1,0,-y0,x1-x0]
        elif orientation==180:
            matrix = [-1,0,0,-1,x1-x0,y1-y0]
        else:
            matrix = [0,1,-1,0,y1-y0,-x0]
        return matrix

class ShowPdfFlowable(Flowable):
    "draws a form xobject in absolute coordinates"
    def __init__(self, formName,orientation=None,iptrans=None):
        Flowable.__init__(self)
        self.formName = formName
        self._orientation = orientation
        self._iptrans = iptrans

    def drawOn(self, canvas, x, y, _sW=0):
        iptrans = self._iptrans
        matrix = self._orientation
        dor = matrix or (iptrans and not iptrans.trivial())
        if dor:
            canvas.saveState()
            if matrix:
                matrix = _formName2OrientationMatrix(canvas,self.formName,matrix)
                if matrix:
                    canvas.transform(*matrix)
            if iptrans:
                if not iptrans.noRotate():
                    canvas.rotate(iptrans.degrees)
                if not iptrans.noTranslate():
                    canvas.translate(iptrans.dx,iptrans.dy)
                if not iptrans.noScale():
                    canvas.scale(iptrans.sx,iptrans.sy)
        #this won't do much if already loaded
        canvas.doForm(self.formName)
        if dor:
            canvas.restoreState()

class IPTrans:
    def __init__(self,sx,sy,dx,dy,degrees):
        self.sx = sx
        self.sy = sy
        self.dx = dx
        self.dy = dy
        self.degrees = degrees

    def trivial(self):
        return self.noScale() and self.noTranslate() and self.noRotate()

    def noScale(self):
        return not (abs(self.sx-1)>1e-8 or abs(self.sy-1)>1e-8)

    def noTranslate(self):
        return not (abs(self.dx)>1e-8 or abs(self.dy)>1e-8)

    def noRotate(self):
        return not abs(self.degrees)>1e-8

def expandPageNumbers(txt):
    """Convert expression like '1,3,7-10,12' to a list of numbers"""
    if not txt: return None

    chunks = txt.split(',')
    pages = []
    pages_append = pages.append
    for chunk in chunks:
        chunk = chunk.strip()
        bits = chunk.split('-')
        if len(bits) == 1:
            pages_append(int(chunk))
        elif len(bits) == 2:
            start = int(bits[0])
            end = int(bits[1])
            for pg in range(start, end+1):
                pages_append(pg)
        else:
            pass
    return pages

class OutlineEntry(Flowable):
    """make an outline entry (lazily convert content)
       should work either in story or in page graphic mode!
    """
    _ZEROSIZE = 1
    _N = 0
    def __init__(self, level, content, closed, name=None, newBookmark=1):
        self.level = level
        self.content = content
        self.closed = closed
        if name is None:
            name = "k%r%d" % (id(self),self._N)     # arbitrary unique string
            self._N += 1
            newBookmark = 1
        self.name = name
        self.newBookmark = newBookmark

    def wrap(self, w, h):
        return 0,0 # consumes no space
    def draw(self):
        "flowable mode"
        self.doEntry(self.canv)
    def __call__(self, canv, doc):
        "graphic mode"
        self.doEntry(canv)
    def doEntry(self, canv):
        content = "".join(map(asUnicodeEx, self.content))
        name = self.name
        if self.newBookmark:
            canv.bookmarkPage(name)
        # bogus level may result in error here
        canv.addOutlineEntry(content, name, self.level, self.closed)
    def __str__(self):
        return ''

def includePdfFlowables(fileName,
                        pages=None,
                        dx=0, dy=0, sx=1, sy=1, degrees=0,
                        orientation=0,
                        isdata=False,       #True if this is a preprocessed data file
                        leadingBreak=True,  #True/False or 'notattop'
                        template=None,
                        outlineText=None,
                        outlineLevel=0,
                        outlineClosed=0,
                        ):
    '''
    includePdfFlowables creates a list of story flowables that
                        represents an included PDF.
    Arguments       meaning
    fileName        string name of a .pdf or .data file
    pages           If None all pages will be used, else this argument can
                    be a string like '1,2,4-6,12-10,15' or an explicit
                    list of integers eg [1,2,7].

    dx,dy,          translation together all these make up a transformation
    sx,sy,          scaling     matrix
    degrees,        rotation

    orientation     integer degrees eg 0 90 270 or 'portrait'/'landscape'
    isdata          True if fileName argument refers to a .data file (as
                    produced by pageCatcher)
    leadingBreak    True/False or 'notattop' specifies whether a leading
                    page break should be used; 'notattop' means a page break
                    will not be used if the story is at the top of a frame.
    template        If specified the index or name of a template to be used.
    outlineText     Any outline text to be used (default None)
    outlineLevel    The level of any outline text.
    outlineClosed   True/False if the outline should be closed or open.
    '''
    try:
        orientation=int(orientation)
        orientation = orientation % 360
    except:
        if orientation=='portrait':
            orientation = 0
        elif orientation=='landscape':
            orientation = 90
        elif orientation!='auto':
            raise ValueError('Bad value %r for orientation attribute' % orientation)

    iptrans = IPTrans(sx,sy,dx,dy,degrees)
    if iptrans.trivial(): iptrans = None

    pages = expandPageNumbers(pages)

    # this one is unusual in that it returns a list of objects to
    # go into the story.
    output = []
    output_append = output.append

    if template:
        output_append(NextPageTemplate(template))

    if isdata:
        pickledStuff = pickle.loads(open_and_read(fileName))
        formNames = pickledStuff[None]
    else:
        #read in the PDF file right now and get the pickled object
        # and names
        pdfContent = open_and_read(fileName)
        prefix = fileName2Prefix(fileName)
        (formNames, pickledStuff) = storeFormsInMemory(
                pdfContent,
                prefix=prefix,
                all=1,
                BBoxes=0,
                extractText=0,
                fformname=None)

    #if explicit pages requested, slim it down.
    if pages:
        newNames = []
        for pgNo in pages:
            newNames.append(formNames[pgNo-1])
        formNames = newNames

    #make object 1 for story
    loader = LoadPdfFlowable(pickledStuff,isdata)
    output_append(loader)

    #now do first page.  This is special as it might
    #have an outline
    formName = formNames[0]
    if leadingBreak:
        output_append((leadingBreak=='notattop' and NotAtTopPageBreak or PageBreak)())
    if outlineText:
        output_append(OutlineEntry(outlineLevel, outlineText, outlineClosed))
    output_append(ShowPdfFlowable(formName,orientation=orientation,iptrans=iptrans))

    #now make a shower for each laterpage, and a page break
    for formName in formNames[1:]:
        output_append(PageBreak())
        output_append(ShowPdfFlowable(formName,orientation=orientation,iptrans=iptrans))

    return output

class includePdfFlowable(Flowable):
    __doc__ = includePdfFlowables.__doc__.replace('s creates',' is a flowable that splits to',1)
    def __init__(self,
                fileName,
                pages=None,
                dx=0, dy=0, sx=1, sy=1, degrees=0,
                orientation=0,
                isdata=False,       #True if this is a preprocessed data file
                leadingBreak=True,  #True/False or 'notattop'
                template=None,
                outlineText=None,
                outlineLevel=0,
                outlineClosed=0,
                ):
        self.fileName = fileName
        self._kwds = dict(
                        pages = pages,
                        dx = dx,
                        dy = dy,
                        sx = sx,
                        sy = sy,
                        degrees = degrees,
                        orientation = orientation,
                        isdata = isdata,
                        leadingBreak = leadingBreak,
                        template = template,
                        outlineText = outlineText,
                        outlineLevel = outlineLevel,
                        outlineClosed = outlineClosed,
                        )
        self._list = None

    def wrap(self,aW,aH):
        '''force a split'''
        return aW, 0x7fffffff

    def split(self,aW,aH):
        if not self._list:
            self._list = includePdfFlowables(self.fileName,**self._kwds)
        return self._list

    def identity(self, maxLen=None):
        msg = "<%s at %s%s> fileName=%s" % (self.__class__.__name__,hex(id(self)),self._frameName(),self.fileName)
        if maxLen:
            return msg[0:maxLen]
        else:
            return msg

def loadPdf(filename, canvas, pageNumbers=None, prefix=None):
    if prefix is None:
        prefix = os.path.splitext(filename)[0] + '_page'
    prefix = prefix.replace('/','_')
    pdfContent = open(filename,"rb").read()
    (formNames, stuff) = storeFormsInMemory(pdfContent,
                                            pagenumbers=pageNumbers,
                                            prefix=prefix,
                                            all=1)

    if pageNumbers:
        namesToInclude = []
        for num in pageNumbers:
            namesToInclude.append(formNames[num])
    else:
        namesToInclude = None
    print('loaded %d pages in %d bytes:' % (len(formNames), len(stuff)))
    restoreFormsInMemory(stuff, canvas,
                         allowDuplicates=1,
                         verbose=1,
                         formnames=namesToInclude)
    return formNames

def test():
    #check for chaos on repeated use
    import reportlab.rl_config
    reportlab.rl_config.invariant = 1

    from reportlab.pdfgen.canvas import Canvas
    c = Canvas('pageflow1.pdf')
    loadPdf("test/ir684.pdf", c)
    c.setFont("Helvetica", 36)
    c.drawString(100,700, "Test restore forms")
    c.showPage()
    c.doForm("test_ir684_page0")
    c.showPage()
    c.doForm("test_ir684_page1")
    c.save()
    print('saved pageflow1.pdf')
    print('\n'*5)

    c = Canvas('pageflow2.pdf')
    loadPdf("test/ir684.pdf", c)
    loadPdf("test/ir684.pdf", c)
    c.setFont("Helvetica", 36)
    c.drawString(100,700, "Test restore forms")
    c.showPage()
    c.doForm("test_ir684_page0")
    c.showPage()
    c.doForm("test_ir684_page1")
    c.save()
    print('saved pageflow2.pdf')

    print('\n'*5)
    c = Canvas('pageflow3.pdf')
    loadPdf("test/ir684.pdf", c, pageNumbers=[0])
    loadPdf("test/ir684.pdf", c, pageNumbers=[1])
    c.setFont("Helvetica", 36)
    c.drawString(100,700, "Test restore forms")
    c.showPage()
    c.doForm("test_ir684_page0")
    c.showPage()
    c.doForm("test_ir684_page1")
    c.save()
    print('saved pageflow3.pdf')

if __name__=='__main__':
    test()
