1 # -*- coding: utf-8 -*-
4 ## This file is part of CDS Indico.
5 ## Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 CERN.
7 ## CDS Indico is free software; you can redistribute it and/or
8 ## modify it under the terms of the GNU General Public License as
9 ## published by the Free Software Foundation; either version 2 of the
10 ## License, or (at your option) any later version.
12 ## CDS Indico is distributed in the hope that it will be useful, but
13 ## WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 ## General Public License for more details.
17 ## You should have received a copy of the GNU General Public License
18 ## along with CDS Indico; if not, write to the Free Software Foundation, Inc.,
19 ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
22 import xml
.sax
.saxutils
as saxutils
23 from HTMLParser
import HTMLParser
24 from reportlab
.platypus
import SimpleDocTemplate
, PageTemplate
, Table
25 from reportlab
.platypus
.tableofcontents
import TableOfContents
26 from reportlab
.lib
.styles
import ParagraphStyle
27 from reportlab
.rl_config
import defaultPageSize
28 from reportlab
.lib
.units
import inch
, cm
29 from reportlab
.lib
.enums
import TA_CENTER
, TA_RIGHT
, TA_LEFT
30 from reportlab
import platypus
31 from reportlab
.pdfgen
.canvas
import Canvas
32 from reportlab
.platypus
.frames
import Frame
33 from reportlab
.lib
.pagesizes
import landscape
, A4
, LETTER
, A0
, A1
, A2
, A3
, A5
34 from reportlab
.pdfbase
import pdfmetrics
35 from reportlab
.pdfbase
.ttfonts
import TTFont
36 from MaKaC
.i18n
import _
37 from MaKaC
.common
.utils
import isStringHTML
39 # PIL is the library used by reportlab to work with images.
40 # If it isn't available, we must NOT put images in the PDF.
41 # Then before add an image, we must check the HAVE_PIL global variable
43 from PIL
import Image
as PILImage
45 except ImportError, e
:
48 ratio
= math
.sqrt(math
.sqrt(2.0))
53 self
.PDFpagesizes
= {'Letter' : LETTER
,
62 self
.PDFfontsizes
= [_("xxx-small"), _("xx-small"), _("x-small"), _("smaller"), _("small"), _("normal"), _("large"), _("larger")]
64 class PDFHTMLParser(HTMLParser
):
65 _removedTags
= ["a", "font"]
68 HTMLParser
.__init
__(self
)
72 "Parse the given string 's'."
75 return "".join(self
.text
)
77 def handle_data(self
, data
):
78 self
.text
.append( saxutils
.escape(data
) )
80 def filterAttrs(self
, attrs
):
83 if x
not in ["target"]:
84 filteredAttrs
.append((x
,y
))
87 def handle_entityref(self
, name
):
88 self
.text
.append( "&%s;"%name
)
90 def handle_starttag(self
, tag
, attrs
):
92 self
.text
.append( "<br/>" )
93 elif tag
in self
._removedTags
:
96 self
.text
.append( "<%s%s>" % (tag
, " ".join([ ' %s="%s"' % (x
,y
) for x
,y
in self
.filterAttrs(attrs
)])) )
98 def handle_startendtag(self
, tag
, attrs
):
99 self
.text
.append( "<%s%s/>" % (tag
, " ".join([ ' %s="%s"' % (x
,y
) for x
,y
in self
.filterAttrs(attrs
)])) )
101 def handle_endtag(self
, tag
):
102 if tag
in self
._removedTags
:
104 self
.text
.append( "</%s>" % tag
)
110 text
= PDFHTMLParser().parse(text
)
111 if not isStringHTML(text
):
112 text
= text
.replace("\r\n"," <br/>")
113 text
= text
.replace("\n"," <br/>")
114 text
= text
.replace("\r"," <br/>")
117 return saxutils
.escape(text
)
119 def modifiedFontSize(fontsize
, lowerNormalHigher
):
121 if lowerNormalHigher
== _("normal"):
123 elif lowerNormalHigher
== _("small"):
124 return fontsize
/ ratio
125 elif lowerNormalHigher
== _("large"):
126 return fontsize
* ratio
127 elif lowerNormalHigher
== _("smaller"):
128 return (fontsize
/ ratio
) / ratio
129 elif lowerNormalHigher
== _("x-small"):
130 return ((fontsize
/ ratio
) / ratio
) / ratio
131 elif lowerNormalHigher
== _("xx-small"):
132 return (((fontsize
/ ratio
) / ratio
) / ratio
) / ratio
133 elif lowerNormalHigher
== _("xxx-small"):
134 return ((((fontsize
/ ratio
) / ratio
) / ratio
) / ratio
) / ratio
135 elif lowerNormalHigher
== _("larger"):
136 return fontsize
* ratio
* ratio
140 alreadyRegistered
= False
143 global alreadyRegistered
144 if not alreadyRegistered
:
145 # Import fonts from indico.extra (separate package)
146 import indico
.extra
.fonts
148 dir=os
.path
.split(os
.path
.abspath(indico
.extra
.fonts
.__file
__))[0]
149 pdfmetrics
.registerFont(TTFont('Times-Roman', os
.path
.join(dir,'LiberationSerif-Regular.ttf')))
150 pdfmetrics
.registerFont(TTFont('Times-Bold', os
.path
.join(dir, 'LiberationSerif-Bold.ttf')))
151 pdfmetrics
.registerFont(TTFont('Times-Italic', os
.path
.join(dir,'LiberationSerif-Italic.ttf')))
152 pdfmetrics
.registerFont(TTFont('Times-Bold-Italic', os
.path
.join(dir, 'LiberationSerif-BoldItalic.ttf')))
153 pdfmetrics
.registerFont(TTFont('Sans', os
.path
.join(dir,'LiberationSans-Regular.ttf')))
154 pdfmetrics
.registerFont(TTFont('Sans-Bold', os
.path
.join(dir, 'LiberationSans-Bold.ttf')))
155 pdfmetrics
.registerFont(TTFont('Sans-Italic', os
.path
.join(dir,'LiberationSans-Italic.ttf')))
156 pdfmetrics
.registerFont(TTFont('Sans-Bold-Italic', os
.path
.join(dir, 'LiberationSans-BoldItalic.ttf')))
157 pdfmetrics
.registerFont(TTFont('Courier', os
.path
.join(dir, 'LiberationMono-Regular.ttf')))
158 pdfmetrics
.registerFont(TTFont('Courier-Bold', os
.path
.join(dir, 'LiberationMono-Bold.ttf')))
159 pdfmetrics
.registerFont(TTFont('Courier-Italic', os
.path
.join(dir, 'LiberationMono-Italic.ttf')))
160 pdfmetrics
.registerFont(TTFont('Courier-Bold-Italic', os
.path
.join(dir, 'LiberationMono-BoldItalic.ttf')))
161 pdfmetrics
.registerFont(TTFont('LinuxLibertine', os
.path
.join(dir, 'LinLibertine_Re-4.4.1.ttf')))
162 pdfmetrics
.registerFont(TTFont('LinuxLibertine-Bold', os
.path
.join(dir, 'LinLibertine_Bd-4.1.0.ttf')))
163 pdfmetrics
.registerFont(TTFont('LinuxLibertine-Italic', os
.path
.join(dir, 'LinLibertine_It-4.0.6.ttf')))
164 pdfmetrics
.registerFont(TTFont('LinuxLibertine-Bold-Italic', os
.path
.join(dir, 'LinLibertine_BI-4.0.5.ttf')))
165 pdfmetrics
.registerFont(TTFont('Kochi-Mincho', os
.path
.join(dir, 'kochi-mincho-subst.ttf')))
166 pdfmetrics
.registerFont(TTFont('Kochi-Gothic', os
.path
.join(dir, 'kochi-gothic-subst.ttf')))
167 #pdfmetrics.registerFont(TTFont('Uming-CN', os.path.join(dir, 'uming.ttc')))
168 alreadyRegistered
= True
170 class Paragraph(platypus
.Paragraph
):
172 add a part attribute for drawing the name of the current part on the laterPages function
174 def __init__(self
, test
, style
, part
="", bulletText
=None, frags
=None, caseSensitive
=1):
175 platypus
.Paragraph
.__init
__(self
, test
, style
, bulletText
, frags
, caseSensitive
)
178 def setPart(self
, part
):
184 class SimpleParagraph(platypus
.Flowable
):
185 """ Simple and fast paragraph.
187 WARNING! This paragraph cannot break the line and doesn't have almost any formatting methods.
188 It's used only to increase PDF performance in places where normal paragraph is not needed.
190 def __init__(self
, text
, fontSize
= 10, indent
= 0, spaceAfter
= 2):
191 platypus
.Flowable
.__init
__(self
)
193 self
.height
= fontSize
+ spaceAfter
194 self
.fontSize
= fontSize
195 self
.spaceAfter
= spaceAfter
203 self
.canv
.setFont('Times-Roman',self
.fontSize
)
204 self
.canv
.drawString(self
.indent
, self
.spaceAfter
, self
.text
)
206 class TableOfContentsEntry(Paragraph
):
208 Class used to create table of contents entry with its number.
209 Much faster than table of table of contents from platypus lib
211 def __init__(self
, test
, pageNumber
,style
, part
="", bulletText
=None, frags
=None, caseSensitive
=1):
212 Paragraph
.__init
__(self
, test
, style
, part
, bulletText
, frags
, caseSensitive
)
214 self
._pageNumber
= pageNumber
218 Draws row of dots from the end of the abstract title to the page number.
221 freeSpace
= int(self
.blPara
.lines
[-1][0])
222 except AttributeError:
223 # Sometimes we get an ABag instead of a tuple.. in this case we use the extraSpace attribute
224 # as it seems to contain just what we need.
225 freeSpace
= int(self
.blPara
.lines
[-1].extraSpace
)
226 while( freeSpace
> 10 ):
227 dot
= self
.beginText(self
.width
+ 10 - freeSpace
, self
.style
.leading
- self
.style
.fontSize
)
229 self
.canv
.drawText(dot
)
233 platypus
.Paragraph
.draw(self
)
234 tx
= self
.beginText(self
.width
+ 10, self
.style
.leading
- self
.style
.fontSize
)
235 tx
.setFont(self
.style
.fontName
, self
.style
.fontSize
, 0)
236 tx
.textLine(str(self
._pageNumber
))
237 self
.canv
.drawText(tx
)
240 class Spacer(platypus
.Spacer
):
241 def __init__(self
, width
, height
, part
=""):
242 platypus
.Spacer
.__init
__(self
, width
, height
)
245 def setPart(self
, part
):
251 class Image(platypus
.Image
):
252 def __init__(self
, filename
, part
="", width
=None, height
=None, kind
='direct', mask
="auto", lazy
=1):
253 platypus
.Image
.__init
__(self
, filename
, width
=None, height
=None, kind
='direct', mask
="auto", lazy
=1)
256 def setPart(self
, part
):
263 class PageBreak(platypus
.PageBreak
):
264 def __init__(self
, part
=""):
267 def setPart(self
, part
):
273 class Preformatted(platypus
.Preformatted
):
274 def __init__(self
, text
, style
, part
="", bulletText
= None, dedent
=0):
275 platypus
.Preformatted
.__init
__(self
, text
, style
, bulletText
= None, dedent
=0)
278 def setPart(self
, part
):
288 self
.name
= "fileDummy"
290 def write(self
, data
):
299 class CanvasA0(Canvas
):
300 def __init__(self
,filename
,
303 pageCompression
=None,
308 Canvas
.__init
__(self
, filename
, pagesize
=pagesize
, bottomup
=bottomup
, pageCompression
=pageCompression
,
309 encoding
=encoding
, invariant
=invariant
, verbosity
=verbosity
)
313 class CanvasA1(Canvas
):
314 def __init__(self
,filename
,
317 pageCompression
=None,
322 Canvas
.__init
__(self
, filename
, pagesize
=pagesize
, bottomup
=bottomup
, pageCompression
=pageCompression
,
323 encoding
=encoding
, invariant
=invariant
, verbosity
=verbosity
)
324 self
.scale(2.0 * math
.sqrt(2.0), 2.0 * math
.sqrt(2.0))
327 class CanvasA2(Canvas
):
328 def __init__(self
,filename
,
331 pageCompression
=None,
336 Canvas
.__init
__(self
, filename
, pagesize
=pagesize
, bottomup
=bottomup
, pageCompression
=pageCompression
,
337 encoding
=encoding
, invariant
=invariant
, verbosity
=verbosity
)
341 class CanvasA3(Canvas
):
342 def __init__(self
,filename
,
345 pageCompression
=None,
350 Canvas
.__init
__(self
, filename
, pagesize
=pagesize
, bottomup
=bottomup
, pageCompression
=pageCompression
,
351 encoding
=encoding
, invariant
=invariant
, verbosity
=verbosity
)
352 self
.scale(math
.sqrt(2.0), math
.sqrt(2.0))
355 class CanvasA5(Canvas
):
356 def __init__(self
,filename
,
359 pageCompression
=None,
364 Canvas
.__init
__(self
, filename
, pagesize
=pagesize
, bottomup
=bottomup
, pageCompression
=pageCompression
,
365 encoding
=encoding
, invariant
=invariant
, verbosity
=verbosity
)
366 self
.scale(1.0 / math
.sqrt(2.0), 1.0 / math
.sqrt(2.0))
369 pagesizeNameToCanvas
= {'A4': Canvas
,
380 def __init__(self
, doc
=None, story
=None, pagesize
= 'A4', printLandscape
=False):
385 #create a new document
386 #As the constructor of SimpleDocTemplate can take only a filename or a file object,
387 #to keep the PDF data not in a file, we use a dummy file object which save the data in a string
388 self
._fileDummy
= FileDummy()
390 self
._doc
= SimpleDocTemplate(self
._fileDummy
, pagesize
= landscape(PDFSizes().PDFpagesizes
[pagesize
]))
392 self
._doc
= SimpleDocTemplate(self
._fileDummy
, pagesize
= PDFSizes().PDFpagesizes
[pagesize
])
394 if story
is not None:
397 #create a new story with a spacer which take all the first page
398 #then the first page is only drawing by the firstPage method
399 self
._story
= [PageBreak()]
402 self
._PAGE
_HEIGHT
= landscape(PDFSizes().PDFpagesizes
[pagesize
])[1]
403 self
._PAGE
_WIDTH
= landscape(PDFSizes().PDFpagesizes
[pagesize
])[0]
405 self
._PAGE
_HEIGHT
= PDFSizes().PDFpagesizes
[pagesize
][1]
406 self
._PAGE
_WIDTH
= PDFSizes().PDFpagesizes
[pagesize
][0]
411 def firstPage(self
, c
, doc
):
412 """set the first page of the document
413 This function is call by doc.build method for the first page
418 def laterPages(self
, c
, doc
):
419 """set the layout of the page after the first
420 This function is call by doc.build method one each page after the first
425 def getBody(self
, story
=None):
428 """add the content to the story
434 #build the pdf in the fileDummy
436 self
._doc
.build(self
._story
, onFirstPage
=self
.firstPage
, onLaterPages
=self
.laterPages
)
437 #return the data from the fileDummy
438 return self
._fileDummy
.getData()
440 def _drawWrappedString(self
, c
, text
, font
='Times-Bold', size
=30, color
=(0,0,0), \
441 align
="center", width
=None, height
=None, measurement
=cm
, lineSpacing
=1, maximumWidth
=None ):
442 if maximumWidth
is None:
443 maximumWidth
= self
._PAGE
_WIDTH
-1*cm
445 width
=self
._PAGE
_WIDTH
/2.0
447 height
=self
._PAGE
_HEIGHT
-10*measurement
448 draw
= c
.drawCentredString
450 draw
= c
.drawRightString
451 elif align
== "left":
453 c
.setFont(font
, size
)
454 c
.setFillColorRGB(*color
)
455 titleWords
= text
.split()
457 for word
in titleWords
:
458 lineAux
= "%s %s"%(line
, word
)
459 lsize
= c
.stringWidth(lineAux
, font
, size
)
460 if lsize
< maximumWidth
:
463 draw(width
,height
, escape(line
))
464 height
-= lineSpacing
*measurement
466 if line
.strip() != "":
467 draw(width
, height
, escape(line
))
470 def _drawLogo(self
, c
, drawTitle
= True):
472 logo
= self
._conf
.getLogo()
475 imagePath
= logo
.getFilePath()
477 img
= PILImage
.open(imagePath
)
478 width
, height
= img
.size
479 if width
> self
._PAGE
_WIDTH
:
480 ratio
= float(height
)/width
481 width
= self
._PAGE
_WIDTH
482 height
= self
._PAGE
_WIDTH
* ratio
483 img
= img
.resize((width
, height
))
484 startHeight
= self
._PAGE
_HEIGHT
486 startHeight
= self
._drawWrappedString
(c
, escape(self
._conf
.getTitle()), height
=self
._PAGE
_HEIGHT
- inch
)
488 c
.drawInlineImage(img
, self
._PAGE
_WIDTH
/2.0 - width
/2, startHeight
- 1.5 * inch
- height
)
494 def _doNothing(canvas
, doc
):
495 "Dummy callback for onPage"
498 class DocTemplateWithTOC(SimpleDocTemplate
):
500 def __init__(self
, indexedFlowable
, filename
, firstPageNumber
= 1, **kw
):
501 """toc is the TableOfContents object
502 indexedFlowale is a dictionnary with flowables as key and a dictionnary as value.
503 the sub-dictionnary have two key:
504 text: the text which will br print in the table
505 level: the level of the entry( modifying the indentation and the police
510 self
._indexedFlowable
= indexedFlowable
511 self
._filename
= filename
513 self
._firstPageNumber
= firstPageNumber
514 SimpleDocTemplate
.__init
__(self
, filename
, **kw
)
517 def afterFlowable(self
, flowable
):
518 if flowable
in self
._indexedFlowable
:
519 self
._toc
.append((self
._indexedFlowable
[flowable
]["level"],self
._indexedFlowable
[flowable
]["text"], self
.page
+ self
._firstPageNumber
- 1))
521 if flowable
.getPart() != "":
522 self
._part
= flowable
.getPart()
526 def handle_documentBegin(self
):
528 SimpleDocTemplate
.handle_documentBegin(self
)
530 def _prepareTOC(self
):
531 headerStyle
= ParagraphStyle({})
532 headerStyle
.fontName
= "Times-Bold"
533 headerStyle
.fontSize
= modifiedFontSize(18, 18)
534 headerStyle
.leading
= modifiedFontSize(22, 22)
535 headerStyle
.alignment
= TA_CENTER
536 entryStyle
= ParagraphStyle({})
537 entryStyle
.spaceBefore
= 8
538 self
._tocStory
.append(PageBreak())
539 self
._tocStory
.append(Spacer(inch
, 1*cm
))
540 self
._tocStory
.append(Paragraph( _("Table of contents"), headerStyle
))
541 self
._tocStory
.append(Spacer(inch
, 2*cm
))
542 for entry
in self
._toc
:
543 self
._tocStory
.append(TableOfContentsEntry("<para leftIndent=%s" % ((entry
[0] - 1) * 50) + ">" + entry
[1] + "</para>", str(entry
[2]),entryStyle
))
544 #self._tocStory.append(SimpleParagraph(entry[1]))
545 self
._tocStory
.append(PageBreak())
547 def multiBuild(self
, story
, filename
=None, canvasMaker
=Canvas
, maxPasses
=10, onFirstPage
=_doNothing
, onLaterPages
=_doNothing
):
548 self
._calc
() #in case we changed margins sizes etc
549 frameT
= Frame(self
.leftMargin
, self
.bottomMargin
, self
.width
, self
.height
, id='normal')
550 self
.addPageTemplates([PageTemplate(id='Later',frames
=frameT
, onPageEnd
=onLaterPages
,pagesize
=self
.pagesize
)])
551 if onLaterPages
is _doNothing
and hasattr(self
,'onLaterPages'):
552 self
.pageTemplates
[0].beforeDrawPage
= self
.onLaterPages
553 SimpleDocTemplate
.multiBuild(self
, story
, maxPasses
, canvasmaker
=canvasMaker
)
555 contentFile
= self
.filename
556 self
.filename
= FileDummy()
557 self
.pageTemplates
= []
558 self
.addPageTemplates([PageTemplate(id='First',frames
=frameT
, onPage
=onFirstPage
,pagesize
=self
.pagesize
)])
559 if onFirstPage
is _doNothing
and hasattr(self
,'onFirstPage'):
560 self
.pageTemplates
[0].beforeDrawPage
= self
.onFirstPage
561 SimpleDocTemplate
.multiBuild(self
, self
._tocStory
, maxPasses
, canvasmaker
=canvasMaker
)
562 self
.mergePDFs(self
.filename
, contentFile
)
564 def mergePDFs(self
, pdf1
, pdf2
):
565 from pyPdf
import PdfFileWriter
, PdfFileReader
567 outputStream
= cStringIO
.StringIO()
568 pdf1Stream
= cStringIO
.StringIO()
569 pdf2Stream
= cStringIO
.StringIO()
570 pdf1Stream
.write(pdf1
.getData())
571 pdf2Stream
.write(pdf2
.getData())
572 output
= PdfFileWriter()
573 background_pages
= PdfFileReader(pdf1Stream
)
574 foreground_pages
= PdfFileReader(pdf2Stream
)
575 for page
in background_pages
.pages
:
577 for page
in foreground_pages
.pages
:
579 output
.write(outputStream
)
580 pdf2
._data
= outputStream
.getvalue()
583 def getCurrentPart(self
):
587 class PDFWithTOC(PDFBase
):
589 create a PDF with a Table of Contents
593 def __init__(self
, story
=None, pagesize
= 'A4', fontsize
= 'normal', firstPageNumber
= 1 ):
596 self
._fontsize
= fontsize
597 #self._indexedFlowable = [] #indexedFlowable
598 #self._toc = TableOfContents()
602 self
._story
.append( Spacer(inch
, 0*cm
) ) #without this blank spacer first abstract isn't displayed. why?
604 #self._toc = TableOfContents()
605 #self._processTOCPage()
606 self
._indexedFlowable
= {}
607 self
._fileDummy
= FileDummy()
609 self
._doc
= DocTemplateWithTOC(self
._indexedFlowable
, self
._fileDummy
, firstPageNumber
= firstPageNumber
, pagesize
=PDFSizes().PDFpagesizes
[pagesize
])
611 self
._PAGE
_HEIGHT
= PDFSizes().PDFpagesizes
[pagesize
][1]
612 self
._PAGE
_WIDTH
= PDFSizes().PDFpagesizes
[pagesize
][0]
617 def _processTOCPage(self
):
618 """ Generates page with table of contents.
620 Not used, because table of contents is generated automatically inside DocTemplateWithTOC class
622 style1
= ParagraphStyle({})
623 style1
.fontName
= "Times-Bold"
624 style1
.fontSize
= modifiedFontSize(18, self
._fontsize
)
625 style1
.leading
= modifiedFontSize(22, self
._fontsize
)
626 style1
.alignment
= TA_CENTER
627 p
= Paragraph( _("Table of contents"), style1
)
628 self
._story
.append(Spacer(inch
, 1*cm
))
629 self
._story
.append(p
)
630 self
._story
.append(Spacer(inch
, 2*cm
))
631 self
._story
.append(self
._toc
)
632 self
._story
.append(PageBreak())
634 def getBody(self
, story
=None):
635 """add the content to the story
636 When you want to put a paragraph p in the toc, add it to the self._indexedFlowable as this:
637 self._indexedFlowable[p] = {"text":"my title", "level":1}
645 self
._doc
.multiBuild( self
._story
, onFirstPage
=self
.firstPage
, onLaterPages
=self
.laterPages
)
646 return self
._fileDummy
.getData()