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 '>' 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