1#!/usr/bin/env python
2#Copyright ReportLab Europe Ltd. 2000-2017
3#see license.txt for license details
4"""
5This is PythonPoint!
6
7The idea is a simple markup languages for describing presentation
8slides, and other documents which run page by page.  I expect most
9of it will be reusable in other page layout stuff.
10
11Look at the sample near the top, which shows how the presentation
12should be coded up.
13
14The parser, which is in a separate module to allow for multiple
15parsers, turns the XML sample into an object tree.  There is a
16simple class hierarchy of items, the inner levels of which create
17flowable objects to go in the frames.  These know how to draw
18themselves.
19
20The currently available 'Presentation Objects' are:
21
22    The main hierarchy...
23        PPPresentation
24        PPSection
25        PPSlide
26        PPFrame
27
28        PPAuthor, PPTitle and PPSubject are optional
29
30    Things to flow within frames...
31        PPPara - flowing text
32        PPPreformatted - text with line breaks and tabs, for code..
33        PPImage
34        PPTable - bulk formatted tabular data
35        PPSpacer
36
37    Things to draw directly on the page...
38        PPRect
39        PPRoundRect
40        PPDrawingElement - user base class for graphics
41        PPLine
42        PPEllipse
43
44Features added by H. Turgut Uyar <uyar@cs.itu.edu.tr>
45- TrueType support (actually, just an import in the style file);
46  this also enables the use of Unicode symbols
47- para, image, table, line, rectangle, roundrect, ellipse, polygon
48  and string elements can now have effect attributes
49  (careful: new slide for each effect!)
50- added printout mode (no new slides for effects, see item above)
51- added a second-level bullet: Bullet2
52- small bugfixes in handleHiddenSlides:
53    corrected the outlineEntry of included hidden slide
54    and made sure to include the last slide even if hidden
55
56Recently added features are:
57
58- file globbing
59- package structure
60- named colors throughout (using names from reportlab/lib/colors.py)
61- handout mode with arbitrary number of columns per page
62- stripped off pages hidden in the outline tree (hackish)
63- new <notes> tag for speaker notes (paragraphs only)
64- new <pycode> tag for syntax-colorized Python code
65- reformatted pythonpoint.xml and monterey.xml demos
66- written/extended DTD
67- arbitrary font support
68- print proper speaker notes (TODO)
69- fix bug with partially hidden graphics (TODO)
70- save in combined presentation/handout mode (TODO)
71- add pyRXP support (TODO)
72"""
73__version__='3.3.0'
74import os, sys, imp, pprint, getopt, glob, re
75
76from reportlab import rl_config
77from reportlab.lib import styles
78from reportlab.lib import colors
79from reportlab.lib.units import cm
80from reportlab.lib.utils import getBytesIO, isStr, isPy3, isBytes, isUnicode
81from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
82from reportlab.pdfbase import pdfmetrics
83from reportlab.pdfgen import canvas
84from reportlab.platypus.doctemplate import SimpleDocTemplate
85from reportlab.platypus.flowables import Flowable
86from reportlab.platypus.xpreformatted import PythonPreformatted
87from reportlab.platypus import Preformatted, Paragraph, Frame, \
88     Image, Table, TableStyle, Spacer
89
90
91USAGE_MESSAGE = """\
92PythonPoint - a tool for making presentations in PDF.
93
94Usage:
95    pythonpoint.py [options] file1.xml [file2.xml [...]]
96
97    where options can be any of these:
98
99        -h / --help     prints this message
100        -n / --notes    leave room for comments
101        -v / --verbose  verbose mode
102        -s / --silent   silent mode (NO output)
103        --handout       produce handout document
104        --printout      produce printout document
105        --cols          specify number of columns
106                        on handout pages (default: 2)
107
108To create the PythonPoint user guide, do:
109    pythonpoint.py pythonpoint.xml
110"""
111
112
113#####################################################################
114# This should probably go into reportlab/lib/fonts.py...
115#####################################################################
116
117class FontNameNotFoundError(Exception):
118    pass
119
120class FontFilesNotFoundError(Exception):
121    pass
122
123##def findFontName(path):
124##    "Extract a Type-1 font name from an AFM file."
125##
126##    f = open(path)
127##
128##    found = 0
129##    while not found:
130##        line = f.readline()[:-1]
131##        if not found and line[:16] == 'StartCharMetrics':
132##            raise FontNameNotFoundError, path
133##        if line[:8] == 'FontName':
134##            fontName = line[9:]
135##            found = 1
136##
137##    return fontName
138##
139##
140##def locateFilesForFontWithName(name):
141##    "Search known paths for AFM/PFB files describing T1 font with given name."
142##
143##    join = os.path.join
144##    splitext = os.path.splitext
145##
146##    afmFile = None
147##    pfbFile = None
148##
149##    found = 0
150##    while not found:
151##        for p in rl_config.T1SearchPath:
152##            afmFiles = glob.glob(join(p, '*.[aA][fF][mM]'))
153##            for f in afmFiles:
154##                T1name = findFontName(f)
155##                if T1name == name:
156##                    afmFile = f
157##                    found = 1
158##                    break
159##            if afmFile:
160##                break
161##        break
162##
163##    if afmFile:
164##        pfbFile = glob.glob(join(splitext(afmFile)[0] + '.[pP][fF][bB]'))[0]
165##
166##    return afmFile, pfbFile
167##
168##
169##def registerFont(name):
170##    "Register Type-1 font for future use."
171##
172##    rl_config.warnOnMissingFontGlyphs = 0
173##    rl_config.T1SearchPath.append(r'C:\Programme\Python21\reportlab\test')
174##
175##    afmFile, pfbFile = locateFilesForFontWithName(name)
176##    if not afmFile and not pfbFile:
177##        raise FontFilesNotFoundError
178##
179##    T1face = pdfmetrics.EmbeddedType1Face(afmFile, pfbFile)
180##    T1faceName = name
181##    pdfmetrics.registerTypeFace(T1face)
182##    T1font = pdfmetrics.Font(name, T1faceName, 'WinAnsiEncoding')
183##    pdfmetrics.registerFont(T1font)
184def registerFont0(sourceFile, name, path):
185    "Register Type-1 font for future use, simple version."
186
187    rl_config.warnOnMissingFontGlyphs = 0
188
189    p = os.path.join(os.path.dirname(sourceFile), path)
190    afmFiles = glob.glob(p + '.[aA][fF][mM]')
191    pfbFiles = glob.glob(p + '.[pP][fF][bB]')
192    assert len(afmFiles) == len(pfbFiles) == 1, FontFilesNotFoundError
193
194    T1face = pdfmetrics.EmbeddedType1Face(afmFiles[0], pfbFiles[0])
195    T1faceName = name
196    pdfmetrics.registerTypeFace(T1face)
197    T1font = pdfmetrics.Font(name, T1faceName, 'WinAnsiEncoding')
198    pdfmetrics.registerFont(T1font)
199
200#####################################################################
201
202
203def checkColor(col):
204    "Converts a color name to an RGB tuple, if possible."
205
206    if isStr(col):
207        if col in dir(colors):
208            col = getattr(colors, col)
209            col = (col.red, col.green, col.blue)
210
211    return col
212
213
214def handleHiddenSlides(slides):
215    """Filters slides from a list of slides.
216
217    In a sequence of hidden slides all but the last one are
218    removed. Also, the slide before the sequence of hidden
219    ones is removed.
220
221    This assumes to leave only those slides in the handout
222    that also appear in the outline, hoping to reduce se-
223    quences where each new slide only adds one new line
224    to a list of items...
225    """
226
227    itd = indicesToDelete = [s.outlineEntry == None for s in slides]
228
229    for i in range(len(itd)-1):
230        if itd[i] == 1:
231            if itd[i+1] == 0:
232                itd[i] = 0
233            if i > 0 and itd[i-1] == 0:
234                itd[i-1] = 1
235
236    itd[len(itd)-1] = 0
237
238    for i in range(len(itd)):
239        if slides[i].outlineEntry:
240            curOutlineEntry = slides[i].outlineEntry
241        if itd[i] == 1:
242            slides[i].delete = 1
243        else:
244            slides[i].outlineEntry = curOutlineEntry
245            slides[i].delete = 0
246
247    slides = [s for s in slides if s.delete == 0]
248
249    return slides
250
251
252def makeSlideTable(slides, pageSize, docWidth, numCols):
253    """Returns a table containing a collection of SlideWrapper flowables.
254    """
255
256    slides = handleHiddenSlides(slides)
257
258    # Set table style.
259    tabStyle = TableStyle(
260        [('GRID', (0,0), (-1,-1), 0.25, colors.black),
261        ('ALIGN', (0,0), (-1,-1), 'CENTRE')
262         ])
263
264    # Build table content.
265    width = docWidth/numCols
266    height = width * pageSize[1]/pageSize[0]
267    matrix = []
268    row = []
269    for slide in slides:
270        sw = SlideWrapper(width, height, slide, pageSize)
271        if (len(row)) < numCols:
272            row.append(sw)
273        else:
274            matrix.append(row)
275            row = []
276            row.append(sw)
277    if len(row) > 0:
278        for i in range(numCols-len(row)):
279            row.append('')
280        matrix.append(row)
281
282    # Make Table flowable.
283    t = Table(matrix,
284              [width + 5]*len(matrix[0]),
285              [height + 5]*len(matrix))
286    t.setStyle(tabStyle)
287
288    return t
289
290
291class SlideWrapper(Flowable):
292    """A Flowable wrapping a PPSlide object.
293    """
294
295    def __init__(self, width, height, slide, pageSize):
296        Flowable.__init__(self)
297        self.width = width
298        self.height = height
299        self.slide = slide
300        self.pageSize = pageSize
301
302
303    def __repr__(self):
304        return "SlideWrapper(w=%s, h=%s)" % (self.width, self.height)
305
306
307    def draw(self):
308        "Draw the slide in our relative coordinate system."
309
310        slide = self.slide
311        pageSize = self.pageSize
312        canv = self.canv
313
314        canv.saveState()
315        canv.scale(self.width/pageSize[0], self.height/pageSize[1])
316        slide.effectName = None
317        slide.drawOn(self.canv)
318        canv.restoreState()
319
320
321class PPPresentation:
322    def __init__(self):
323        self.sourceFilename = None
324        self.filename = None
325        self.outDir = None
326        self.description = None
327        self.title = None
328        self.author = None
329        self.subject = None
330        self.notes = 0          # different printing mode
331        self.handout = 0        # prints many slides per page
332        self.printout = 0       # remove hidden slides
333        self.cols = 0           # columns per handout page
334        self.slides = []
335        self.effectName = None
336        self.showOutline = 1   #should it be displayed when opening?
337        self.compression = rl_config.pageCompression
338        self.pageDuration = None
339        #assume landscape
340        self.pageWidth = rl_config.defaultPageSize[1]
341        self.pageHeight = rl_config.defaultPageSize[0]
342        self.verbose = rl_config.verbose
343
344
345    def saveAsPresentation(self):
346        """Write the PDF document, one slide per page."""
347        if self.verbose:
348            print('saving presentation...')
349        pageSize = (self.pageWidth, self.pageHeight)
350        if self.sourceFilename:
351            filename = os.path.splitext(self.sourceFilename)[0] + '.pdf'
352        if self.outDir: filename = os.path.join(self.outDir,os.path.basename(filename))
353        if self.verbose:
354            print(filename)
355        #canv = canvas.Canvas(filename, pagesize = pageSize)
356        outfile = getBytesIO()
357        if self.notes:
358            #translate the page from landscape to portrait
359            pageSize= pageSize[1], pageSize[0]
360        canv = canvas.Canvas(outfile, pagesize = pageSize)
361        canv.setPageCompression(self.compression)
362        canv.setPageDuration(self.pageDuration)
363        if self.title:
364            canv.setTitle(self.title)
365        if self.author:
366            canv.setAuthor(self.author)
367        if self.subject:
368            canv.setSubject(self.subject)
369
370        slideNo = 0
371        for slide in self.slides:
372            #need diagnostic output if something wrong with XML
373            slideNo = slideNo + 1
374            if self.verbose:
375                print('doing slide %d, id = %s' % (slideNo, slide.id))
376            if self.notes:
377                #frame and shift the slide
378                #canv.scale(0.67, 0.67)
379                scale_amt = (min(pageSize)/float(max(pageSize)))*.95
380                #canv.translate(self.pageWidth / 6.0, self.pageHeight / 3.0)
381                #canv.translate(self.pageWidth / 2.0, .025*self.pageHeight)
382                canv.translate(.025*self.pageHeight, (self.pageWidth/2.0) + 5)
383                #canv.rotate(90)
384                canv.scale(scale_amt, scale_amt)
385                canv.rect(0,0,self.pageWidth, self.pageHeight)
386            slide.drawOn(canv)
387            canv.showPage()
388
389        #ensure outline visible by default
390        if self.showOutline:
391            canv.showOutline()
392
393        canv.save()
394        return self.savetofile(outfile, filename)
395
396
397    def saveAsHandout(self):
398        """Write the PDF document, multiple slides per page."""
399
400        styleSheet = getSampleStyleSheet()
401        h1 = styleSheet['Heading1']
402        bt = styleSheet['BodyText']
403
404        if self.sourceFilename :
405            filename = os.path.splitext(self.sourceFilename)[0] + '.pdf'
406
407        outfile = getBytesIO()
408        doc = SimpleDocTemplate(outfile, pagesize=rl_config.defaultPageSize, showBoundary=0)
409        doc.leftMargin = 1*cm
410        doc.rightMargin = 1*cm
411        doc.topMargin = 2*cm
412        doc.bottomMargin = 2*cm
413        multiPageWidth = rl_config.defaultPageSize[0] - doc.leftMargin - doc.rightMargin - 50
414
415        story = []
416        orgFullPageSize = (self.pageWidth, self.pageHeight)
417        t = makeSlideTable(self.slides, orgFullPageSize, multiPageWidth, self.cols)
418        story.append(t)
419
420##        #ensure outline visible by default
421##        if self.showOutline:
422##            doc.canv.showOutline()
423
424        doc.build(story)
425        return self.savetofile(outfile, filename)
426
427    def savetofile(self, pseudofile, filename):
428        """Save the pseudo file to disk and return its content as a
429        string of text."""
430        pseudofile.flush()
431        content = pseudofile.getvalue()
432        pseudofile.close()
433        if filename :
434            outf = open(filename, "wb")
435            outf.write(content)
436            outf.close()
437        return content
438
439
440
441    def save(self):
442        "Save the PDF document."
443
444        if self.handout:
445            return self.saveAsHandout()
446        else:
447            return self.saveAsPresentation()
448
449
450#class PPSection:
451#   """A section can hold graphics which will be drawn on all
452#   pages within it, before frames and other content are done.
453#  In other words, a background template."""
454#    def __init__(self, name):
455#       self.name = name
456#        self.graphics = []
457#
458#    def drawOn(self, canv):
459#        for graphic in self.graphics:
460###            graphic.drawOn(canv)
461#
462#            name = str(hash(graphic))
463#            internalname = canv._doc.hasForm(name)
464#
465#            canv.saveState()
466#            if not internalname:
467#                canv.beginForm(name)
468#                graphic.drawOn(canv)
469#                canv.endForm()
470#                canv.doForm(name)
471#            else:
472#                canv.doForm(name)
473#            canv.restoreState()
474
475
476definedForms = {}
477
478class PPSection:
479    """A section can hold graphics which will be drawn on all
480    pages within it, before frames and other content are done.
481    In other words, a background template."""
482
483    def __init__(self, name):
484        self.name = name
485        self.graphics = []
486
487    def drawOn(self, canv):
488        for graphic in self.graphics:
489            graphic.drawOn(canv)
490            continue
491            name = str(hash(graphic))
492            #internalname = canv._doc.hasForm(name)
493            if name in definedForms:
494                internalname = 1
495            else:
496                internalname = None
497                definedForms[name] = 1
498            if not internalname:
499                canv.beginForm(name)
500                canv.saveState()
501                graphic.drawOn(canv)
502                canv.restoreState()
503                canv.endForm()
504                canv.doForm(name)
505            else:
506                canv.doForm(name)
507
508
509class PPNotes:
510    def __init__(self):
511        self.content = []
512
513    def drawOn(self, canv):
514        print(self.content)
515
516
517class PPSlide:
518    def __init__(self):
519        self.id = None
520        self.title = None
521        self.outlineEntry = None
522        self.outlineLevel = 0   # can be higher for sub-headings
523        self.effectName = None
524        self.effectDirection = 0
525        self.effectDimension = 'H'
526        self.effectMotion = 'I'
527        self.effectDuration = 1
528        self.frames = []
529        self.notes = []
530        self.graphics = []
531        self.section = None
532
533    def drawOn(self, canv):
534        if self.effectName:
535            canv.setPageTransition(
536                        effectname=self.effectName,
537                        direction = self.effectDirection,
538                        dimension = self.effectDimension,
539                        motion = self.effectMotion,
540                        duration = self.effectDuration
541                        )
542
543        if self.outlineEntry:
544            #gets an outline automatically
545            self.showOutline = 1
546            #put an outline entry in the left pane
547            tag = self.title
548            canv.bookmarkPage(tag)
549            canv.addOutlineEntry(tag, tag, self.outlineLevel)
550
551        if self.section:
552            self.section.drawOn(canv)
553
554        for graphic in self.graphics:
555            graphic.drawOn(canv)
556
557        for frame in self.frames:
558            frame.drawOn(canv)
559
560##        # Need to draw the notes *somewhere*...
561##        for note in self.notes:
562##            print note
563
564
565class PPFrame:
566    def __init__(self, x, y, width, height):
567        self.x = x
568        self.y = y
569        self.width = width
570        self.height = height
571        self.content = []
572        self.showBoundary = 0
573
574    def drawOn(self, canv):
575        #make a frame
576        frame = Frame( self.x,
577                              self.y,
578                              self.width,
579                              self.height
580                              )
581        frame.showBoundary = self.showBoundary
582
583        #build a story for the frame
584        story = []
585        for thingy in self.content:
586            #ask it for any flowables
587            story.append(thingy.getFlowable())
588        #draw it
589        frame.addFromList(story,canv)
590
591
592class PPPara:
593    """This is a placeholder for a paragraph."""
594    def __init__(self):
595        self.rawtext = ''
596        self.style = None
597
598    def escapeAgain(self, text):
599        """The XML has been parsed once, so '&gt;' became '>'
600        in rawtext.  We need to escape this to get back to
601        something the Platypus parser can accept"""
602        pass
603
604    def getFlowable(self):
605        p = Paragraph(
606                    self.rawtext,
607                    getStyles()[self.style],
608                    self.bulletText
609                    )
610        return p
611
612
613class PPPreformattedText:
614    """Use this for source code, or stuff you do not want to wrap"""
615    def __init__(self):
616        self.rawtext = ''
617        self.style = None
618
619    def getFlowable(self):
620        return Preformatted(self.rawtext, getStyles()[self.style])
621
622
623class PPPythonCode:
624    """Use this for colored Python source code"""
625    def __init__(self):
626        self.rawtext = ''
627        self.style = None
628
629    def getFlowable(self):
630        return PythonPreformatted(self.rawtext, getStyles()[self.style])
631
632
633class PPImage:
634    """Flowing image within the text"""
635    def __init__(self):
636        self.filename = None
637        self.width = None
638        self.height = None
639
640    def getFlowable(self):
641        return Image(self.filename, self.width, self.height)
642
643
644class PPTable:
645    """Designed for bulk loading of data for use in presentations."""
646    def __init__(self):
647        self.rawBlocks = [] #parser stuffs things in here...
648        self.fieldDelim = ','  #tag args can override
649        self.rowDelim = '\n'   #tag args can override
650        self.data = None
651        self.style = None  #tag args must specify
652        self.widths = None  #tag args can override
653        self.heights = None #tag args can override
654
655    def getFlowable(self):
656        self.parseData()
657        t = Table(
658                self.data,
659                self.widths,
660                self.heights)
661        if self.style:
662            t.setStyle(getStyles()[self.style])
663
664        return t
665
666    def parseData(self):
667        """Try to make sense of the table data!"""
668        rawdata = ''.join(self.rawBlocks).strip()
669        lines = rawdata.split(self.rowDelim)
670        #clean up...
671        lines = [line.strip() for line in lines]
672        self.data = []
673        for line in lines:
674            cells = line.split(self.fieldDelim)
675            self.data.append(cells)
676
677        #get the width list if not given
678        if not self.widths:
679            self.widths = [None] * len(self.data[0])
680        if not self.heights:
681            self.heights = [None] * len(self.data)
682
683##        import pprint
684##        print 'table data:'
685##        print 'style=',self.style
686##        print 'widths=',self.widths
687##        print 'heights=',self.heights
688##        print 'fieldDelim=',repr(self.fieldDelim)
689##        print 'rowDelim=',repr(self.rowDelim)
690##        pprint.pprint(self.data)
691
692
693class PPSpacer:
694    def __init__(self):
695        self.height = 24  #points
696
697    def getFlowable(self):
698        return Spacer(72, self.height)
699
700
701    #############################################################
702    #
703    #   The following are things you can draw on a page directly.
704    #
705    ##############################################################
706
707##class PPDrawingElement:
708##    """Base class for something which you draw directly on the page."""
709##    def drawOn(self, canv):
710##        raise NotImplementedError("Abstract base class!")
711
712
713class PPFixedImage:
714    """You place this on the page, rather than flowing it"""
715    def __init__(self):
716        self.filename = None
717        self.x = 0
718        self.y = 0
719        self.width = None
720        self.height = None
721
722    def drawOn(self, canv):
723        if self.filename:
724            x, y = self.x, self.y
725            w, h = self.width, self.height
726            canv.drawImage(self.filename, x, y, w, h)
727
728
729class PPRectangle:
730    def __init__(self, x, y, width, height):
731        self.x = x
732        self.y = y
733        self.width = width
734        self.height = height
735        self.fillColor = None
736        self.strokeColor = (1,1,1)
737        self.lineWidth=0
738
739    def drawOn(self, canv):
740        canv.saveState()
741        canv.setLineWidth(self.lineWidth)
742        if self.fillColor:
743            r,g,b = checkColor(self.fillColor)
744            canv.setFillColorRGB(r,g,b)
745        if self.strokeColor:
746            r,g,b = checkColor(self.strokeColor)
747            canv.setStrokeColorRGB(r,g,b)
748        canv.rect(self.x, self.y, self.width, self.height,
749                    stroke=(self.strokeColor!=None),
750                    fill = (self.fillColor!=None)
751                    )
752        canv.restoreState()
753
754
755class PPRoundRect:
756    def __init__(self, x, y, width, height, radius):
757        self.x = x
758        self.y = y
759        self.width = width
760        self.height = height
761        self.radius = radius
762        self.fillColor = None
763        self.strokeColor = (1,1,1)
764        self.lineWidth=0
765
766    def drawOn(self, canv):
767        canv.saveState()
768        canv.setLineWidth(self.lineWidth)
769        if self.fillColor:
770            r,g,b = checkColor(self.fillColor)
771            canv.setFillColorRGB(r,g,b)
772        if self.strokeColor:
773            r,g,b = checkColor(self.strokeColor)
774            canv.setStrokeColorRGB(r,g,b)
775        canv.roundRect(self.x, self.y, self.width, self.height,
776                    self.radius,
777                    stroke=(self.strokeColor!=None),
778                    fill = (self.fillColor!=None)
779                    )
780        canv.restoreState()
781
782
783class PPLine:
784    def __init__(self, x1, y1, x2, y2):
785        self.x1 = x1
786        self.y1 = y1
787        self.x2 = x2
788        self.y2 = y2
789        self.fillColor = None
790        self.strokeColor = (1,1,1)
791        self.lineWidth=0
792
793    def drawOn(self, canv):
794        canv.saveState()
795        canv.setLineWidth(self.lineWidth)
796        if self.strokeColor:
797            r,g,b = checkColor(self.strokeColor)
798            canv.setStrokeColorRGB(r,g,b)
799        canv.line(self.x1, self.y1, self.x2, self.y2)
800        canv.restoreState()
801
802
803class PPEllipse:
804    def __init__(self, x1, y1, x2, y2):
805        self.x1 = x1
806        self.y1 = y1
807        self.x2 = x2
808        self.y2 = y2
809        self.fillColor = None
810        self.strokeColor = (1,1,1)
811        self.lineWidth=0
812
813    def drawOn(self, canv):
814        canv.saveState()
815        canv.setLineWidth(self.lineWidth)
816        if self.strokeColor:
817            r,g,b = checkColor(self.strokeColor)
818            canv.setStrokeColorRGB(r,g,b)
819        if self.fillColor:
820            r,g,b = checkColor(self.fillColor)
821            canv.setFillColorRGB(r,g,b)
822        canv.ellipse(self.x1, self.y1, self.x2, self.y2,
823                    stroke=(self.strokeColor!=None),
824                    fill = (self.fillColor!=None)
825                     )
826        canv.restoreState()
827
828
829class PPPolygon:
830    def __init__(self, pointlist):
831        self.points = pointlist
832        self.fillColor = None
833        self.strokeColor = (1,1,1)
834        self.lineWidth=0
835
836    def drawOn(self, canv):
837        canv.saveState()
838        canv.setLineWidth(self.lineWidth)
839        if self.strokeColor:
840            r,g,b = checkColor(self.strokeColor)
841            canv.setStrokeColorRGB(r,g,b)
842        if self.fillColor:
843            r,g,b = checkColor(self.fillColor)
844            canv.setFillColorRGB(r,g,b)
845
846        path = canv.beginPath()
847        (x,y) = self.points[0]
848        path.moveTo(x,y)
849        for (x,y) in self.points[1:]:
850            path.lineTo(x,y)
851        path.close()
852        canv.drawPath(path,
853                      stroke=(self.strokeColor!=None),
854                      fill=(self.fillColor!=None))
855        canv.restoreState()
856
857
858class PPString:
859    def __init__(self, x, y):
860        self.text = ''
861        self.x = x
862        self.y = y
863        self.align = TA_LEFT
864        self.font = 'Times-Roman'
865        self.size = 12
866        self.color = (0,0,0)
867        self.hasInfo = 0  # these can have data substituted into them
868
869    def normalizeText(self):
870        """It contains literal XML text typed over several lines.
871        We want to throw away
872        tabs, newlines and so on, and only accept embedded string
873        like '\n'"""
874        lines = self.text.split('\n')
875        newtext = []
876        for line in lines:
877            newtext.append(line.strip())
878        #accept all the '\n' as newlines
879
880        self.text = newtext
881
882    def drawOn(self, canv):
883        # for a string in a section, this will be drawn several times;
884        # so any substitution into the text should be in a temporary
885        # variable
886        if self.hasInfo:
887            # provide a dictionary of stuff which might go into
888            # the string, so they can number pages, do headers
889            # etc.
890            info = {}
891            info['title'] = canv._doc.info.title
892            info['author'] = canv._doc.info.author
893            info['subject'] = canv._doc.info.subject
894            info['page'] = canv.getPageNumber()
895            drawText = self.text % info
896        else:
897            drawText = self.text
898
899        if self.color is None:
900            return
901        lines = drawText.strip().split('\\n')
902        canv.saveState()
903
904        canv.setFont(self.font, self.size)
905
906        r,g,b = checkColor(self.color)
907        canv.setFillColorRGB(r,g,b)
908        cur_y = self.y
909        for line in lines:
910            if self.align == TA_LEFT:
911                canv.drawString(self.x, cur_y, line)
912            elif self.align == TA_CENTER:
913                canv.drawCentredString(self.x, cur_y, line)
914            elif self.align == TA_RIGHT:
915                canv.drawRightString(self.x, cur_y, line)
916            cur_y = cur_y - 1.2*self.size
917
918        canv.restoreState()
919
920class PPDrawing:
921    def __init__(self):
922        self.drawing = None
923    def getFlowable(self):
924        return self.drawing
925
926class PPFigure:
927    def __init__(self):
928        self.figure = None
929    def getFlowable(self):
930        return self.figure
931
932def getSampleStyleSheet():
933    from tools.pythonpoint.styles.standard import getParagraphStyles
934    return getParagraphStyles()
935
936def toolsDir():
937    import tools
938    return tools.__path__[0]
939
940#make a singleton and a function to access it
941_styles = None
942def getStyles():
943    global _styles
944    if not _styles:
945        _styles = getSampleStyleSheet()
946    return _styles
947
948
949def setStyles(newStyleSheet):
950    global _styles
951    _styles = newStyleSheet
952
953_pyRXP_Parser = None
954def validate(rawdata):
955    global _pyRXP_Parser
956    if not _pyRXP_Parser:
957        try:
958            import pyRXP
959        except ImportError:
960            return
961        from reportlab.lib.utils import open_and_read, rl_isfile
962        dtd = 'pythonpoint.dtd'
963        if not rl_isfile(dtd):
964            dtd = os.path.join(toolsDir(),'pythonpoint','pythonpoint.dtd')
965            if not rl_isfile(dtd): return
966        def eocb(URI,dtdText=open_and_read(dtd),dtd=dtd):
967            if os.path.basename(URI)=='pythonpoint.dtd': return dtd,dtdText
968            return URI
969        _pyRXP_Parser = pyRXP.Parser(eoCB=eocb)
970    return _pyRXP_Parser.parse(rawdata)
971
972
973def _re_match(pat,text,flags=re.M|re.I):
974    if isPy3 and isBytes(text):
975            pat = pat.encode('latin1')
976    return re.match(pat,text,flags)
977
978def process(datafile, notes=0, handout=0, printout=0, cols=0, verbose=0, outDir=None, datafilename=None, fx=1):
979    "Process one PythonPoint source file."
980    if not hasattr(datafile, "read"):
981        if not datafilename: datafilename = datafile
982        datafile = open(datafile,'rb')
983    else:
984        if not datafilename: datafilename = "PseudoFile"
985    rawdata = datafile.read()
986    if not isUnicode(rawdata):
987        encs = ['utf8','iso-8859-1']
988        m=_re_match(r'^\s*(<\?xml[^>]*\?>)',rawdata)
989        if m:
990            m1=_re_match(r"""^.*\sencoding\s*=\s*("[^"]*"|'[^']*')""",m.group(1))
991            if m1:
992                enc = m1.group(1)[1:-1]
993                if enc:
994                    if enc in encs:
995                        encs.remove(enc)
996                    encs.insert(0,enc)
997        for enc in encs:
998            try:
999                udata = rawdata.decode(enc)
1000                break
1001            except:
1002                pass
1003        else:
1004            raise ValueError('cannot decode input data')
1005    else:
1006        udata = rawdata
1007    if isPy3:
1008        rawdata = udata
1009    else:
1010        rawdata = udata.encode('utf8')
1011
1012    #if pyRXP present, use it to check and get line numbers for errors...
1013    validate(rawdata)
1014    return _process(rawdata, datafilename, notes, handout, printout, cols, verbose, outDir, fx)
1015
1016def _process(rawdata, datafilename, notes=0, handout=0, printout=0, cols=0, verbose=0, outDir=None, fx=1):
1017    #print 'inner process fx=%d' % fx
1018    from tools.pythonpoint.stdparser import PPMLParser
1019    parser = PPMLParser()
1020    parser.fx = fx
1021    parser.sourceFilename = datafilename
1022    parser.feed(rawdata)
1023    pres = parser.getPresentation()
1024    pres.sourceFilename = datafilename
1025    pres.outDir = outDir
1026    pres.notes = notes
1027    pres.handout = handout
1028    pres.printout = printout
1029    pres.cols = cols
1030    pres.verbose = verbose
1031
1032    if printout:
1033        pres.slides = handleHiddenSlides(pres.slides)
1034
1035    #this does all the work
1036    pdfcontent = pres.save()
1037
1038    if verbose:
1039        print('saved presentation %s.pdf' % os.path.splitext(datafilename)[0])
1040    parser.close()
1041
1042    return pdfcontent
1043##class P:
1044##    def feed(self, text):
1045##        parser = stdparser.PPMLParser()
1046##        d = pyRXP.parse(text)
1047##
1048##
1049##def process2(datafilename, notes=0, handout=0, cols=0):
1050##    "Process one PythonPoint source file."
1051##
1052##    import pyRXP, pprint
1053##
1054##    rawdata = open(datafilename).read()
1055##    d = pyRXP.parse(rawdata)
1056##    pprint.pprint(d)
1057
1058
1059def handleOptions():
1060    # set defaults
1061    from reportlab import rl_config
1062    options = {'cols':2,
1063               'handout':0,
1064               'printout':0,
1065               'help':0,
1066               'notes':0,
1067               'fx':1,
1068               'verbose':rl_config.verbose,
1069               'silent':0,
1070               'outDir': None}
1071
1072    args = sys.argv[1:]
1073    args = [x for x in args if x and x[0]=='-'] + [x for x in args if not x or x[0]!='-']
1074    try:
1075        shortOpts = 'hnvsx'
1076        longOpts = 'cols= outdir= handout help notes printout verbose silent nofx'.split()
1077        optList, args = getopt.getopt(args, shortOpts, longOpts)
1078    except getopt.error as msg:
1079        options['help'] = 1
1080
1081    if not args and os.path.isfile('pythonpoint.xml'):
1082        args = ['pythonpoint.xml']
1083
1084    # Remove leading dashes (max. two).
1085    for i in range(len(optList)):
1086        o, v = optList[i]
1087        while o[0] == '-':
1088            o = o[1:]
1089        optList[i] = (o, v)
1090
1091        if o == 'cols': options['cols'] = int(v)
1092        elif o=='outdir': options['outDir'] = v
1093
1094    if [ov for ov in optList if ov[0] == 'handout']:
1095        options['handout'] = 1
1096
1097    if [ov for ov in optList if ov[0] == 'printout']:
1098        options['printout'] = 1
1099
1100    if optList == [] and args == [] or \
1101       [ov for ov in optList if ov[0] in ('h', 'help')]:
1102        options['help'] = 1
1103
1104    if [ov for ov in optList if ov[0] in ('n', 'notes')]:
1105        options['notes'] = 1
1106
1107    if [ov for ov in optList if ov[0] in ('x', 'nofx')]:
1108        options['fx'] = 0
1109
1110    if [ov for ov in optList if ov[0] in ('v', 'verbose')]:
1111        options['verbose'] = 1
1112
1113    #takes priority over verbose.  Used by our test suite etc.
1114        #to ensure no output at all
1115    if [ov for ov in optList if ov[0] in ('s', 'silent')]:
1116        options['silent'] = 1
1117        options['verbose'] = 0
1118
1119
1120    return options, args
1121
1122def main():
1123    options, args = handleOptions()
1124
1125    if options['help']:
1126        print(USAGE_MESSAGE)
1127        sys.exit(0)
1128
1129    if options['verbose'] and options['notes']:
1130        print('speaker notes mode')
1131
1132    if options['verbose'] and options['handout']:
1133        print('handout mode')
1134
1135    if options['verbose'] and options['printout']:
1136        print('printout mode')
1137
1138    if not options['fx']:
1139        print('suppressing special effects')
1140    for fileGlobs in args:
1141        files = glob.glob(fileGlobs)
1142        if not files:
1143            print(fileGlobs, "not found")
1144            return
1145        for datafile in files:
1146            if os.path.isfile(datafile):
1147                file = os.path.join(os.getcwd(), datafile)
1148                notes, handout, printout, cols, verbose, fx = options['notes'], options['handout'], options['printout'],  options['cols'], options['verbose'], options['fx']
1149                process(file, notes, handout, printout, cols, verbose, options['outDir'], fx=fx)
1150            else:
1151                print('Data file not found:', datafile)
1152
1153if __name__ == '__main__':
1154    main()
1155