1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
|
#Copyright ReportLab Europe Ltd. 2000-2012
#see license.txt for license details
__version__=''' $Id$ '''
#tests and documents Page Layout API
__doc__="""This is not obvious so here's a brief explanation. This module is both
the test script and user guide for layout. Each page has two frames on it:
one for commentary, and one for demonstration objects which may be drawn in
various esoteric ways. The two functions getCommentary() and getExamples()
return the 'story' for each. The run() function gets the stories, then
builds a special "document model" in which the frames are added to each page
and drawn into.
"""
from reportlab.lib.testutils import setOutDir,makeSuiteForClasses, outputfile, printLocation
setOutDir(__name__)
import copy, sys, os
from reportlab.pdfgen import canvas
from reportlab import platypus
from reportlab.platypus import BaseDocTemplate, PageTemplate, Flowable, FrameBreak
from reportlab.platypus import Paragraph, Preformatted
from reportlab.lib.units import inch, cm
from reportlab.lib.styles import PropertySet, getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.rl_config import defaultPageSize
from reportlab.lib.utils import haveImages, _RL_DIR, rl_isfile, open_for_read, fileName2FSEnc
import unittest
from reportlab.lib.testutils import testsFolder
if haveImages:
_GIF = os.path.join(testsFolder,'pythonpowered.gif')
if not rl_isfile(_GIF): _GIF = None
else:
_GIF = None
if _GIF: _GIFFSEnc=fileName2FSEnc(_GIF)
_JPG = os.path.join(testsFolder,'..','docs','images','lj8100.jpg')
if not rl_isfile(_JPG): _JPG = None
def getFurl(fn):
furl = fn.replace(os.sep,'/')
if sys.platform=='win32' and furl[1]==':': furl = furl[0]+'|'+furl[2:]
if furl[0]!='/': furl = '/'+furl
return 'file://'+furl
PAGE_HEIGHT = defaultPageSize[1]
#################################################################
#
# first some drawing utilities
#
#
################################################################
BASEFONT = ('Times-Roman', 10)
def framePage(canvas,doc):
#canvas.drawImage("snkanim.gif", 36, 36)
canvas.saveState()
canvas.setStrokeColorRGB(1,0,0)
canvas.setLineWidth(5)
canvas.line(66,72,66,PAGE_HEIGHT-72)
canvas.setFont('Times-Italic',12)
canvas.drawRightString(523, PAGE_HEIGHT - 56, "Platypus User Guide and Test Script")
canvas.setFont('Times-Roman',12)
canvas.drawString(4 * inch, 0.75 * inch,
"Page %d" % canvas.getPageNumber())
canvas.restoreState()
def getParagraphs(textBlock):
"""Within the script, it is useful to whack out a page in triple
quotes containing separate paragraphs. This breaks one into its
constituent paragraphs, using blank lines as the delimiter."""
lines = textBlock.split('\n')
paras = []
currentPara = []
for line in lines:
if len(line.strip()) == 0:
#blank, add it
if currentPara != []:
paras.append('\n'.join(currentPara))
currentPara = []
else:
currentPara.append(line)
#...and the last one
if currentPara != []:
paras.append('\n'.join(currentPara))
return paras
def getCommentary():
"""Returns the story for the commentary - all the paragraphs."""
styleSheet = getSampleStyleSheet()
story = []
story.append(Paragraph("""
PLATYPUS User Guide and Test Script
""", styleSheet['Heading1']))
spam = """
Welcome to PLATYPUS!
Platypus stands for "Page Layout and Typography Using Scripts". It is a high
level page layout library which lets you programmatically create complex
documents with a minimum of effort.
This document is both the user guide & the output of the test script.
In other words, a script used platypus to create the document you are now
reading, and the fact that you are reading it proves that it works. Or
rather, that it worked for this script anyway. It is a first release!
Platypus is built 'on top of' PDFgen, the Python library for creating PDF
documents. To learn about PDFgen, read the document testpdfgen.pdf.
"""
for text in getParagraphs(spam):
story.append(Paragraph(text, styleSheet['BodyText']))
story.append(Paragraph("""
What concepts does PLATYPUS deal with?
""", styleSheet['Heading2']))
story.append(Paragraph("""
The central concepts in PLATYPUS are Flowable Objects, Frames, Flow
Management, Styles and Style Sheets, Paragraphs and Tables. This is
best explained in contrast to PDFgen, the layer underneath PLATYPUS.
PDFgen is a graphics library, and has primitive commans to draw lines
and strings. There is nothing in it to manage the flow of text down
the page. PLATYPUS works at the conceptual level fo a desktop publishing
package; you can write programs which deal intelligently with graphic
objects and fit them onto the page.
""", styleSheet['BodyText']))
story.append(Paragraph("""
How is this document organized?
""", styleSheet['Heading2']))
story.append(Paragraph("""
Since this is a test script, we'll just note how it is organized.
the top of each page contains commentary. The bottom half contains
example drawings and graphic elements to whicht he commentary will
relate. Down below, you can see the outline of a text frame, and
various bits and pieces within it. We'll explain how they work
on the next page.
""", styleSheet['BodyText']))
story.append(FrameBreak())
#######################################################################
# Commentary Page 2
#######################################################################
story.append(Paragraph("""
Flowable Objects
""", styleSheet['Heading2']))
spam = """
The first and most fundamental concept is that of a 'Flowable Object'.
In PDFgen, you draw stuff by calling methods of the canvas to set up
the colors, fonts and line styles, and draw the graphics primitives.
If you set the pen color to blue, everything you draw after will be
blue until you change it again. And you have to handle all of the X-Y
coordinates yourself.
A 'Flowable object' is exactly what it says. It knows how to draw itself
on the canvas, and the way it does so is totally independent of what
you drew before or after. Furthermore, it draws itself at the location
on the page you specify.
The most fundamental Flowable Objects in most documents are likely to be
paragraphs, tables, diagrams/charts and images - but there is no
restriction. You can write your own easily, and I hope that people
will start to contribute them. PINGO users - we provide a "PINGO flowable" object to let
you insert platform-independent graphics into the flow of a document.
When you write a flowable object, you inherit from Flowable and
must implement two methods. object.wrap(availWidth, availHeight) will be called by other parts of
the system, and tells you how much space you have. You should return
how much space you are going to use. For a fixed-size object, this
is trivial, but it is critical - PLATYPUS needs to figure out if things
will fit on the page before drawing them. For other objects such as paragraphs,
the height is obviously determined by the available width.
The second method is object.draw(). Here, you do whatever you want.
The Flowable base class sets things up so that you have an origin of
(0,0) for your drawing, and everything will fit nicely if you got the
height and width right. It also saves and restores the graphics state
around your calls, so you don;t have to reset all the properties you
changed.
Programs which actually draw a Flowable don't
call draw() this directly - they call object.drawOn(canvas, x, y).
So you can write code in your own coordinate system, and things
can be drawn anywhere on the page (possibly even scaled or rotated).
"""
for text in getParagraphs(spam):
story.append(Paragraph(text, styleSheet['BodyText']))
story.append(FrameBreak())
#######################################################################
# Commentary Page 3
#######################################################################
story.append(Paragraph("""
Available Flowable Objects
""", styleSheet['Heading2']))
story.append(Paragraph("""
Platypus comes with a basic set of flowable objects. Here we list their
class names and tell you what they do:
""", styleSheet['BodyText']))
#we can use the bullet feature to do a definition list
story.append(Paragraph("""
<para color=green bcolor=red bg=pink>This is a <font bgcolor=yellow color=red>contrived</font> object to give an example of a Flowable -
just a fixed-size box with an X through it and a centred string.</para>""",
styleSheet['Definition'],
bulletText='XBox ' #hack - spot the extra space after
))
story.append(Paragraph("""
This is the basic unit of a document. Paragraphs can be finely
tuned and offer a host of properties through their associated
ParagraphStyle.""",
styleSheet['Definition'],
bulletText='Paragraph ' #hack - spot the extra space after
))
story.append(Paragraph("""
This is used for printing code and other preformatted text.
There is no wrapping, and line breaks are taken where they occur.
Many paragraph style properties do not apply. You may supply
an optional 'dedent' parameter to trim a number of characters
off the front of each line.""",
styleSheet['Definition'],
bulletText='Preformatted ' #hack - spot the extra space after
))
story.append(Paragraph("""
This is a straight wrapper around an external image file. By default
the image will be drawn at a scale of one pixel equals one point, and
centred in the frame. You may supply an optional width and height.""",
styleSheet['Definition'],
bulletText='Image ' #hack - spot the extra space after
))
story.append(Paragraph("""
This is a table drawing class; it is intended to be simpler
than a full HTML table model yet be able to draw attractive output,
and behave intelligently when the numbers of rows and columns vary.
Still need to add the cell properties (shading, alignment, font etc.)""",
styleSheet['Definition'],
bulletText='Table ' #hack - spot the extra space after
))
story.append(Paragraph("""
This is a 'null object' which merely takes up space on the page.
Use it when you want some extra padding betweene elements.""",
styleSheet['Definition'],
bulletText='Spacer ' #hack - spot the extra space after
))
story.append(Paragraph("""
A FrameBreak causes the document to call its handle_frameEnd method.""",
styleSheet['Definition'],
bulletText='FrameBreak ' #hack - spot the extra space after
))
story.append(Paragraph("""
This is in progress, but a macro is basically a chunk of Python code to
be evaluated when it is drawn. It could do lots of neat things.""",
styleSheet['Definition'],
bulletText='Macro ' #hack - spot the extra space after
))
story.append(FrameBreak())
story.append(Paragraph(
"The next example uses a custom font",
styleSheet['Italic']))
def code(txt,story=story,styleSheet=styleSheet):
story.append(Preformatted(txt,styleSheet['Code']))
code('''import reportlab.rl_config
reportlab.rl_config.warnOnMissingFontGlyphs = 0
from reportlab.pdfbase import pdfmetrics
fontDir = os.path.join(_RL_DIR,'fonts')
face = pdfmetrics.EmbeddedType1Face(os.path.join(fontDir,'DarkGardenMK.afm'),
os.path.join(fontDir,'DarkGardenMK.pfb'))
faceName = face.name # should be 'DarkGardenMK'
pdfmetrics.registerTypeFace(face)
font = pdfmetrics.Font(faceName, faceName, 'WinAnsiEncoding')
pdfmetrics.registerFont(font)
# put it inside a paragraph.
story.append(Paragraph(
"""This is an ordinary paragraph, which happens to contain
text in an embedded font:
<font name="DarkGardenMK">DarkGardenMK</font>.
Now for the real challenge...""", styleSheet['Normal']))
styRobot = ParagraphStyle('Robot', styleSheet['Normal'])
styRobot.fontSize = 16
styRobot.leading = 20
styRobot.fontName = 'DarkGardenMK'
story.append(Paragraph(
"This whole paragraph is 16-point DarkGardenMK.",
styRobot))''')
story.append(FrameBreak())
if _GIF:
story.append(Paragraph("""We can use images via the file name""", styleSheet['BodyText']))
code(''' story.append(platypus.Image('%s'))'''%_GIFFSEnc)
code(''' story.append(platypus.Image(fileName2FSEnc('%s')))''' % _GIFFSEnc)
story.append(Paragraph("""They can also be used with a file URI or from an open python file!""", styleSheet['BodyText']))
code(''' story.append(platypus.Image('%s'))'''% getFurl(_GIFFSEnc))
code(''' story.append(platypus.Image(open_for_read('%s','b')))''' % _GIFFSEnc)
story.append(FrameBreak())
story.append(Paragraph("""Images can even be obtained from the internet.""", styleSheet['BodyText']))
code(''' img = platypus.Image('http://www.reportlab.com/rsrc/encryption.gif')
story.append(img)''')
story.append(FrameBreak())
if _JPG:
story.append(Paragraph("""JPEGs are a native PDF image format. They should be available even if PIL cannot be used.""", styleSheet['BodyText']))
story.append(FrameBreak())
return story
def getExamples():
"""Returns all the example flowable objects"""
styleSheet = getSampleStyleSheet()
story = []
#make a style with indents and spacing
sty = ParagraphStyle('obvious', None)
sty.leftIndent = 18
sty.rightIndent = 18
sty.firstLineIndent = 18
sty.spaceBefore = 6
sty.spaceAfter = 6
story.append(Paragraph("""Now for some demo stuff - we need some on this page,
even before we explain the concepts fully""", styleSheet['BodyText']))
p = Paragraph("""
Platypus is all about fitting objects into frames on the page. You
are looking at a fairly simple Platypus paragraph in Debug mode.
It has some gridlines drawn around it to show the left and right indents,
and the space before and after, all of which are attributes set in
the style sheet. To be specific, this paragraph has left and
right indents of 18 points, a first line indent of 36 points,
and 6 points of space before and after itself. A paragraph
object fills the width of the enclosing frame, as you would expect.""", sty)
p.debug = 1 #show me the borders
story.append(p)
story.append(Paragraph("""Same but with justification 1.5 extra leading and green text.""", styleSheet['BodyText']))
p = Paragraph("""
<para align=justify leading="+1.5" fg=green><font color=red>Platypus</font> is all about fitting objects into frames on the page. You
are looking at a fairly simple Platypus paragraph in Debug mode.
It has some gridlines drawn around it to show the left and right indents,
and the space before and after, all of which are attributes set in
the style sheet. To be specific, this paragraph has left and
right indents of 18 points, a first line indent of 36 points,
and 6 points of space before and after itself. A paragraph
object fills the width of the enclosing frame, as you would expect.</para>""", sty)
p.debug = 1 #show me the borders
story.append(p)
story.append(platypus.XBox(4*inch, 0.75*inch,
'This is a box with a fixed size'))
story.append(Paragraph("""
All of this is being drawn within a text frame which was defined
on the page. This frame is in 'debug' mode so you can see the border,
and also see the margins which it reserves. A frame does not have
to have margins, but they have been set to 6 points each to create
a little space around the contents.
""", styleSheet['BodyText']))
story.append(FrameBreak())
#######################################################################
# Examples Page 2
#######################################################################
story.append(Paragraph("""
Here's the base class for Flowable...
""", styleSheet['Italic']))
code = '''class Flowable:
"""Abstract base class for things to be drawn. Key concepts:
1. It knows its size
2. It draws in its own coordinate system (this requires the
base API to provide a translate() function.
"""
def __init__(self):
self.width = 0
self.height = 0
self.wrapped = 0
def drawOn(self, canvas, x, y):
"Tell it to draw itself on the canvas. Do not override"
self.canv = canvas
self.canv.saveState()
self.canv.translate(x, y)
self.draw() #this is the bit you overload
self.canv.restoreState()
del self.canv
def wrap(self, availWidth, availHeight):
"""This will be called by the enclosing frame before objects
are asked their size, drawn or whatever. It returns the
size actually used."""
return (self.width, self.height)
'''
story.append(Preformatted(code, styleSheet['Code'], dedent=4))
story.append(FrameBreak())
#######################################################################
# Examples Page 3
#######################################################################
story.append(Paragraph(
"Here are some examples of the remaining objects above.",
styleSheet['Italic']))
story.append(Paragraph("This is a bullet point", styleSheet['Bullet'], bulletText='O'))
story.append(Paragraph("Another bullet point", styleSheet['Bullet'], bulletText='O'))
story.append(Paragraph("""Here is a Table, which takes all kinds of formatting options...""",
styleSheet['Italic']))
story.append(platypus.Spacer(0, 12))
g = platypus.Table(
(('','North','South','East','West'),
('Quarter 1',100,200,300,400),
('Quarter 2',100,200,300,400),
('Total',200,400,600,800)),
(72,36,36,36,36),
(24, 16,16,18)
)
style = platypus.TableStyle([('ALIGN', (1,1), (-1,-1), 'RIGHT'),
('ALIGN', (0,0), (-1,0), 'CENTRE'),
('GRID', (0,0), (-1,-1), 0.25, colors.black),
('LINEBELOW', (0,0), (-1,0), 2, colors.black),
('LINEBELOW',(1,-1), (-1, -1), 2, (0.5, 0.5, 0.5)),
('TEXTCOLOR', (0,1), (0,-1), colors.black),
('BACKGROUND', (0,0), (-1,0), (0,0.7,0.7))
])
g.setStyle(style)
story.append(g)
story.append(FrameBreak())
#######################################################################
# Examples Page 4 - custom fonts
#######################################################################
# custom font with LettError-Robot font
import reportlab.rl_config
reportlab.rl_config.warnOnMissingFontGlyphs = 0
from reportlab.pdfbase import pdfmetrics
fontDir = os.path.join(_RL_DIR,'fonts')
face = pdfmetrics.EmbeddedType1Face(os.path.join(fontDir,'DarkGardenMK.afm'),os.path.join(fontDir,'DarkGardenMK.pfb'))
faceName = face.name # should be 'DarkGardenMK'
pdfmetrics.registerTypeFace(face)
font = pdfmetrics.Font(faceName, faceName, 'WinAnsiEncoding')
pdfmetrics.registerFont(font)
# put it inside a paragraph.
story.append(Paragraph(
"""This is an ordinary paragraph, which happens to contain
text in an embedded font:
<font name="DarkGardenMK">DarkGardenMK</font>.
Now for the real challenge...""", styleSheet['Normal']))
styRobot = ParagraphStyle('Robot', styleSheet['Normal'])
styRobot.fontSize = 16
styRobot.leading = 20
styRobot.fontName = 'DarkGardenMK'
story.append(Paragraph(
"This whole paragraph is 16-point DarkGardenMK.",
styRobot))
story.append(FrameBreak())
if _GIF:
story.append(Paragraph("Here is an Image flowable obtained from a string filename.",styleSheet['Italic']))
story.append(platypus.Image(_GIF))
story.append(Paragraph( "Here is an Image flowable obtained from a utf8 filename.", styleSheet['Italic']))
#story.append(platypus.Image(fileName2FSEnc(_GIF)))
story.append(Paragraph("Here is an Image flowable obtained from a string file url.",styleSheet['Italic']))
story.append(platypus.Image(getFurl(_GIF)))
story.append(Paragraph("Here is an Image flowable obtained from an open file.",styleSheet['Italic']))
story.append(platypus.Image(open_for_read(_GIF,'b')))
story.append(FrameBreak())
try:
img = platypus.Image('http://www.reportlab.com/rsrc/encryption.gif')
story.append(Paragraph("Here is an Image flowable obtained from a string http url.",styleSheet['Italic']))
story.append(img)
except:
story.append(Paragraph("The image could not be obtained from a string http url.",styleSheet['Italic']))
story.append(FrameBreak())
if _JPG:
img = platypus.Image(_JPG)
story.append(Paragraph("Here is an JPEG Image flowable obtained from a filename.",styleSheet['Italic']))
story.append(img)
story.append(Paragraph("Here is an JPEG Image flowable obtained from an open file.",styleSheet['Italic']))
img = platypus.Image(open_for_read(_JPG,'b'))
story.append(img)
story.append(FrameBreak())
return story
class AndyTemplate(BaseDocTemplate):
_invalidInitArgs = ('pageTemplates',)
def __init__(self, filename, **kw):
frame1 = platypus.Frame(inch, 5.6*inch, 6*inch, 5.2*inch,id='F1')
frame2 = platypus.Frame(inch, inch, 6*inch, 4.5*inch, showBoundary=1,id='F2')
self.allowSplitting = 0
BaseDocTemplate.__init__(self,filename,**kw)
self.addPageTemplates(PageTemplate('normal',[frame1,frame2],framePage))
def fillFrame(self,flowables):
f = self.frame
while len(flowables)>0 and f is self.frame:
self.handle_flowable(flowables)
def build(self, flowables1, flowables2):
assert [x for x in flowables1 if not isinstance(x,Flowable)]==[], "flowables1 argument error"
assert [x for x in flowables2 if not isinstance(x,Flowable)]==[], "flowables2 argument error"
self._startBuild()
while (len(flowables1) > 0 or len(flowables1) > 0):
self.clean_hanging()
self.fillFrame(flowables1)
self.fillFrame(flowables2)
self._endBuild()
def showProgress(pageNo):
print('CALLBACK SAYS: page %d' % pageNo)
def run():
doc = AndyTemplate(outputfile('test_platypus_general.pdf'),subject='test0')
#doc.setPageCallBack(showProgress)
commentary = getCommentary()
examples = getExamples()
doc.build(commentary,examples)
class PlatypusTestCase(unittest.TestCase):
"Make documents with lots of Platypus features"
def test0(self):
"Make a platypus document"
run()
def test1(self):
#test from Wietse Jacobs
from reportlab.lib.styles import ParagraphStyle
from reportlab.graphics.shapes import Drawing, Rect
from reportlab.platypus import SimpleDocTemplate
normal = ParagraphStyle(name='Normal', fontName='Helvetica', fontSize=8.5, leading=11)
header = ParagraphStyle(name='Heading1', parent=normal, fontSize=14, leading=19,
spaceAfter=6, keepWithNext=1)
d = Drawing(400, 200)
d.add(Rect(50, 50, 300, 100))
story = [Paragraph("The section header", header), d,
]
doc = SimpleDocTemplate(outputfile('test_drawing_keepwithnext.pdf'))
doc.build(story)
def makeSuite():
return makeSuiteForClasses(PlatypusTestCase)
#noruntests
if __name__ == "__main__":
if '-debug' in sys.argv:
run()
else:
unittest.TextTestRunner().run(makeSuite())
printLocation()
|