4 :Author: Engelbert Gruber
5 :Contact: goodger@users.sourceforge.net
8 :Copyright: This module has been placed in the public domain.
12 The output uses reportlabs module.
14 Some stylesheet is needed.
17 __docformat__
= 'reStructuredText'
21 from types
import ListType
, TupleType
, UnicodeType
22 from docutils
import writers
, nodes
, languages
24 from stylesheet
import getStyleSheet
25 from rltemplate
import RLDocTemplate
27 from reportlab
.lib
.styles
import ParagraphStyle
28 from reportlab
.lib
.enums
import *
29 from reportlab
.lib
.pagesizes
import A4
30 from reportlab
.platypus
import *
31 from reportlab
.platypus
.para
import Paragraph
32 from reportlab
.lib
import colors
33 from reportlab
.lib
.units
import inch
34 from StringIO
import StringIO
36 class Writer(writers
.Writer
):
38 supported
= ('pdf','rlpdf')
39 """Formats this writer supports."""
42 'PDF-Specific Options',
44 (('Format for footnote references: one of "superscript" or '
45 '"brackets". Default is "brackets".',
46 ['--footnote-references'],
47 {'choices': ['superscript', 'brackets'], 'default': 'brackets',
48 'metavar': '<FORMAT>'}),))
51 """Final translated form of `document`."""
54 writers
.Writer
.__init
__(self
)
55 self
.translator_class
= PDFTranslator
58 visitor
= self
.translator_class(self
.document
)
59 self
.document
.walkabout(visitor
)
60 self
.story
= visitor
.as_what()
61 self
.output
= self
.record()
64 from reportlab
.platypus
import SimpleDocTemplate
66 doc
= SimpleDocTemplate(out
, pagesize
=A4
)
73 class PDFTranslator(nodes
.NodeVisitor
):
75 def __init__(self
, doctree
):
76 self
.settings
= settings
= doctree
.settings
77 self
.styleSheet
= getStyleSheet()
78 nodes
.NodeVisitor
.__init
__(self
, doctree
)
79 self
.language
= languages
.get_language(doctree
.settings
.language_code
)
80 self
.in_docinfo
= None
88 self
.bulletText
= '\267' # maybe move this into stylesheet.
94 def encode(self
, text
):
95 """Encode special characters in `text` & return."""
96 if type(text
) is UnicodeType
:
97 text
= text
.replace(u
'\u2020', u
' ')
98 text
= text
.replace(u
'\xa0', u
' ')
99 text
= text
.encode('utf-8')
100 #text = text.replace("&", "&")
101 #text = text.replace("<", '"')
102 #text = text.replace('"', "(quot)")
103 #text = text.replace(">", '"')
104 # footnotes have character values above 128 ?
107 def append_para(self
, text
, in_style
='Normal', bulletText
=None):
108 if type(text
) in (ListType
, TupleType
):
109 text
= ''.join([self
.encode(t
) for t
in text
])
110 style
= self
.styleSheet
[in_style
]
111 self
.story
.append(Paragraph(self
.encode(text
), style
, \
112 bulletText
=bulletText
, context
=self
.styleSheet
))
114 def starttag(self
, node
, tagname
, suffix
='\n', **attributes
):
116 for (name
, value
) in attributes
.items():
117 atts
[name
.lower()] = value
118 for att
in ('class',): # append to node attribute
119 if node
.has_key(att
):
120 if atts
.has_key(att
):
121 atts
[att
] = node
[att
] + ' ' + atts
[att
]
122 for att
in ('id',): # node attribute overrides
123 if node
.has_key(att
):
124 atts
[att
] = node
[att
]
125 attlist
= atts
.items()
128 for name
, value
in attlist
:
129 if value
is None: # boolean attribute
130 parts
.append(name
.lower())
131 elif isinstance(value
, ListType
):
132 values
= [str(v
) for v
in value
]
133 parts
.append('%s="%s"' % (name
.lower(),
134 self
.encode(' '.join(values
))))
136 parts
.append('%s="%s"' % (name
.lower(),
137 self
.encode(str(value
))))
138 return '<%s>%s' % (' '.join(parts
), suffix
)
140 def visit_Text(self
, node
):
141 self
.context
.append('#text')
142 self
.body
.append(node
.astext())
144 def depart_Text(self
, node
):
147 def visit_admonition(self
, node
, name
):
150 def depart_admonition(self
):
153 def visit_attention(self
, node
):
154 self
.visit_admonition(node
, 'attention')
156 def depart_attention(self
, node
):
157 self
.depart_admonition()
159 def visit_author(self
, node
):
160 self
.visit_docinfo_item(node
, 'author')
162 def depart_author(self
, node
):
163 self
.depart_docinfo_item()
165 def visit_address(self
, node
):
166 self
.visit_docinfo_item(node
, 'address')
168 def depart_address(self
, node
):
169 self
.depart_docinfo_item()
171 def visit_version(self
, node
):
172 self
.visit_docinfo_item(node
, 'version')
174 def depart_version(self
, node
):
175 self
.depart_docinfo_item()
177 def visit_system_message(self
, node
):
180 def depart_system_message(self
, node
):
183 def visit_term(self
, node
):
184 self
.context
.append('dt')
185 self
.body
.append(self
.starttag(node
, 'dt', ''))
187 def depart_term(self
, node
):
188 # Closes on visit_definition
191 def visit_authors(self
, node
):
194 def depart_authors(self
, node
):
197 def visit_block_quote(self
, node
):
200 def depart_block_quote(self
, node
):
203 def visit_bullet_list(self
, node
):
204 self
.context
.append(len(self
.body
))
205 self
.context
.append('ul')
206 self
.body
.append('<ul bulletText="%s">' % self
.bulletText
)
208 def depart_bullet_list(self
, node
):
210 self
.body
.append('</ul>')
211 start
= self
.context
.pop()
212 if not 'ul' in self
.context
:
213 self
.append_para(self
.body
[start
:])
214 self
.body
= self
.body
[:start
]
216 def visit_caption(self
, node
):
219 def depart_caption(self
, node
):
222 def visit_caution(self
, node
):
223 self
.visit_admonition(node
, 'caution')
225 def depart_caution(self
, node
):
226 self
.depart_admonition()
228 def visit_citation(self
, node
):
231 def depart_citation(self
, node
):
234 def visit_citation_reference(self
, node
):
237 def depart_citation_reference(self
, node
):
240 def visit_classifier(self
, node
):
243 def depart_classifier(self
, node
):
246 def visit_colspec(self
, node
):
249 def depart_colspec(self
, node
):
252 def visit_contact(self
, node
):
253 self
.visit_docinfo_item(node
, 'contact')
255 def depart_contact(self
, node
):
256 self
.depart_docinfo_item()
258 def visit_copyright(self
, node
):
259 self
.visit_docinfo_item(node
, 'copyright')
261 def depart_copyright(self
, node
):
262 self
.depart_docinfo_item()
264 def visit_danger(self
, node
):
265 self
.visit_admonition(node
, 'danger')
267 def depart_danger(self
, node
):
268 self
.depart_admonition()
270 def visit_date(self
, node
):
271 self
.visit_docinfo_item(node
, 'date')
273 def depart_date(self
, node
):
274 self
.depart_docinfo_item()
276 def visit_definition(self
, node
):
277 self
.body
.append('</dt>')
278 self
.context
.append('dd')
279 self
.body
.append(self
.starttag(node
, 'dd'))
281 def depart_definition(self
, node
):
283 self
.body
.append('</dd>')
285 def visit_definition_list(self
, node
):
286 self
.context
.append(len(self
.body
))
287 self
.context
.append('dl')
288 self
.body
.append(self
.starttag(node
, 'dl'))
290 def depart_definition_list(self
, node
):
292 self
.body
.append('</dl>')
293 start
= self
.context
.pop()
294 if not 'dl' in self
.context
:
295 self
.append_para(self
.body
[start
:])
296 self
.body
= self
.body
[:start
]
298 def visit_definition_list_item(self
, node
):
301 def depart_definition_list_item(self
, node
):
304 def visit_description(self
, node
):
307 def depart_description(self
, node
):
310 def visit_docinfo(self
, node
):
311 self
.context
.append(len(self
.body
))
314 def depart_docinfo(self
, node
):
315 start
= self
.context
.pop()
316 docinfo
= self
.body
[start
:]
317 self
.body
= self
.body
[:start
]
318 self
.append_para(docinfo
)
319 self
.in_docinfo
= None
321 def visit_docinfo_item(self
, node
, name
):
322 self
.body
.append('<para style="DocInfo"><b>%s: </b>' % self
.language
.labels
[name
])
324 def depart_docinfo_item(self
):
325 self
.body
.append('</para>')
327 def visit_doctest_block(self
, node
):
328 self
.visit_literal_block(node
)
330 def depart_doctest_block(self
, node
):
331 self
.depart_literal_block(node
)
333 def visit_line_block(self
, node
):
334 self
.visit_literal_block(node
)
336 def depart_line_block(self
, node
):
337 self
.depart_literal_block(node
)
339 def visit_document(self
, node
):
342 def depart_document(self
, node
):
345 def visit_emphasis(self
, node
):
346 self
.context
.append('i')
347 self
.body
.append('<i>')
349 def depart_emphasis(self
, node
):
351 self
.body
.append('</i>')
353 def visit_entry(self
, node
):
356 def depart_entry(self
, node
):
359 def visit_enumerated_list(self
, node
):
360 self
.context
.append(len(self
.body
))
361 self
.context
.append('ol')
362 self
.body
.append('<ol>')
364 def depart_enumerated_list(self
, node
):
366 self
.body
.append('</ol>')
367 start
= self
.context
.pop()
368 if not 'ol' in self
.context
:
369 self
.append_para(self
.body
[start
:])
370 self
.body
= self
.body
[:start
]
372 def visit_error(self
, node
):
373 self
.visit_admonition(node
, 'error')
375 def depart_error(self
, node
):
376 self
.depart_admonition()
378 def visit_field(self
, node
):
379 self
.body
.append('<para>')
381 def depart_field(self
, node
):
382 self
.body
.append('</para>')
384 def visit_field_argument(self
, node
):
387 def depart_field_argument(self
, node
):
390 def visit_field_list(self
, node
):
391 self
.context
.append(len(self
.body
))
392 self
.body
.append('<para>')
394 def depart_field_list(self
, node
):
395 start
= self
.context
.pop()
396 self
.body
.append('</para>')
397 self
.append_para(self
.body
[start
:])
398 self
.body
= self
.body
[:start
]
400 def visit_field_name(self
, node
):
401 self
.body
.append('<b>')
403 def depart_field_name(self
, node
):
404 self
.body
.append(': </b>')
406 def visit_field_body(self
, node
):
409 def depart_field_body(self
, node
):
412 def visit_footnote(self
, node
):
413 self
.context
.append('footnotes')
414 self
.footnote_backrefs(node
)
416 def footnote_backrefs(self
, node
):
417 if self
.settings
.footnote_backlinks
and node
.hasattr('backrefs'):
418 backrefs
= node
['backrefs']
419 if len(backrefs
) == 1:
420 self
.context
.append("%s%s" % (self
.starttag({}, 'setLink', '', destination
=node
['id']), \
422 self
.context
.append("%s%s" % (self
.starttag({}, 'link', '', destination
=backrefs
[0]), \
427 for backref
in backrefs
:
428 backlinks
.append("%s%s%s" % (self
.starttag({}, 'link', '', destination
=backref
),
432 self
.context
.append(' <i>(%s)</i> ' % ', '.join(backlinks
))
433 self
.context
.append("%s%s" % (self
.starttag({}, 'setLink', '', destination
=node
['id']), \
436 self
.context
.append("%s%s" % (self
.starttag({}, 'setLink', '', destination
=node
['id']), \
438 self
.context
.append('')
440 def footnote_backrefs_depart(self
, node
):
441 if not self
.context
and self
.body
:
442 self
.append_para(self
.body
)
445 def depart_footnote(self
, node
):
447 self
.footnote_backrefs_depart(node
)
449 def visit_footnote_reference(self
, node
):
451 if self
.settings
.footnote_backlinks
and node
.has_key('id'):
452 self
.body
.append(self
.starttag(node
, 'setLink', '', destination
=node
['id']))
453 self
.context
.append('</setLink>')
455 self
.context
.append('')
458 if node
.has_key('refid'):
460 elif node
.has_key('refname'):
461 href
= self
.document
.nameids
[node
['refname']]
462 format
= self
.settings
.footnote_references
463 if format
== 'brackets':
465 self
.context
.append(']')
466 elif format
== 'superscript':
468 self
.context
.append('</super>')
469 else: # shouldn't happen
471 self
.content
.append('???')
472 self
.body
.append(self
.starttag(node
, 'link', suffix
, destination
=href
))
474 def depart_footnote_reference(self
, node
):
475 self
.body
.append(self
.context
.pop())
476 self
.body
.append('</link>')
477 self
.body
.append(self
.context
.pop())
479 def visit_hint(self
, node
):
480 self
.visit_admonition(node
, 'hint')
482 def depart_hint(self
, node
):
483 self
.depart_admonition()
485 def visit_image(self
, node
):
488 def depart_image(self
, node
):
491 def visit_important(self
, node
):
492 self
.visit_admonition(node
, 'important')
494 def depart_important(self
, node
):
495 self
.depart_admonition()
497 def visit_interpreted(self
, node
):
500 def depart_interpreted(self
, node
):
503 def visit_label(self
, node
):
504 if 'footnotes' in self
.context
:
505 self
.body
.append('[')
507 def depart_label(self
, node
):
508 if 'footnotes' in self
.context
:
509 self
.body
.append(']')
510 self
.body
.append(self
.context
.pop())
511 self
.body
.append(self
.context
.pop())
512 self
.body
.append(' ')
514 def visit_legend(self
, node
):
517 def depart_legend(self
, node
):
520 def visit_list_item(self
, node
):
521 self
.context
.append('li')
522 self
.body
.append('<li>')
524 def depart_list_item(self
, node
):
526 self
.body
.append('</li>')
528 def visit_literal(self
, node
):
529 self
.context
.append('literal')
531 def depart_literal(self
, node
):
534 def visit_literal_block(self
, node
):
535 self
.story
.append(Preformatted(node
.astext(), self
.styleSheet
['Code']))
538 def depart_literal_block(self
, node
):
541 def visit_meta(self
, node
):
542 self
.head
.append(self
.starttag(node
, 'meta', **node
.attributes
))
544 def depart_meta(self
, node
):
547 def visit_note(self
, node
):
548 self
.visit_admonition(node
, 'note')
550 def depart_note(self
, node
):
551 self
.depart_admonition()
553 def visit_option(self
, node
):
556 def depart_option(self
, node
):
559 def visit_option_argument(self
, node
):
562 def depart_option_argument(self
, node
):
565 def visit_option_group(self
, node
):
568 def depart_option_group(self
, node
):
571 def visit_option_list(self
, node
):
572 self
.context
.append(len(self
.body
))
573 self
.context
.append('option_list')
575 def depart_option_list(self
, node
):
577 start
= self
.context
.pop()
578 if not 'option_list' in self
.context
:
579 self
.append_para(self
.body
[start
:])
580 self
.body
= self
.body
[:start
]
582 def visit_option_list_item(self
, node
):
585 def depart_option_list_item(self
, node
):
588 def visit_option_string(self
, node
):
591 def depart_option_string(self
, node
):
594 def visit_organization(self
, node
):
595 self
.visit_docinfo_item(node
, 'organization')
597 def depart_organization(self
, node
):
598 self
.depart_docinfo_item()
600 def visit_paragraph(self
, node
):
601 self
.context
.append(len(self
.body
))
602 self
.context
.append('p')
604 def depart_paragraph(self
, node
):
606 start
= self
.context
.pop()
607 if not self
.context
and self
.body
:
608 self
.append_para(self
.body
[start
:])
609 self
.body
= self
.body
[:start
]
611 def visit_problematic(self
, node
):
614 def depart_problematic(self
, node
):
617 def visit_raw(self
, node
):
618 if node
.has_key('format') and node
['format'] == 'html':
619 self
.body
.append(node
.astext())
622 def visit_target(self
, node
):
623 if not (node
.has_key('refuri') or node
.has_key('refid')
624 or node
.has_key('refname')):
626 if node
.has_key('id'):
628 elif node
.has_key('name'):
630 self
.body
.append("%s%s" % (self
.starttag(node
, 'setLink', '', destination
=href
), \
634 def depart_target(self
, node
):
637 def visit_reference(self
, node
):
638 self
.context
.append('a')
639 if node
.has_key('refuri'):
640 href
= node
['refuri']
641 self
.body
.append(self
.starttag(node
, 'a', '', href
=href
))
642 self
.context
.append('</a>')
644 if node
.has_key('id'):
645 self
.body
.append(self
.starttag({}, 'setLink', '', destination
=node
['id']))
646 self
.context
.append('</setLink>')
647 if node
.has_key('refid'):
649 elif node
.has_key('refname'):
650 href
= self
.document
.nameids
[node
['refname']]
651 self
.body
.append(self
.starttag(node
, 'link', '', destination
=href
))
652 self
.context
.append('</link>')
654 def depart_reference(self
, node
):
655 if node
.has_key('id') and \
656 not node
.has_key('refuri'):
657 self
.body
.append(self
.context
.pop())
658 self
.body
.append(self
.context
.pop())
661 def visit_revision(self
, node
):
662 self
.visit_docinfo_item(node
, 'revision')
664 def depart_revision(self
, node
):
665 self
.depart_docinfo_item()
667 def visit_row(self
, node
):
670 def depart_row(self
, node
):
673 def visit_section(self
, node
):
674 self
.sectionlevel
+= 1
676 def depart_section(self
, node
):
677 self
.sectionlevel
-= 1
679 def visit_status(self
, node
):
680 self
.visit_docinfo_item(node
, 'status')
682 def depart_status(self
, node
):
683 self
.depart_docinfo_item()
685 def visit_strong(self
, node
):
686 self
.context
.append('b')
687 self
.body
.append('<b>')
689 def depart_strong(self
, node
):
691 self
.body
.append('</b>')
693 def visit_subtitle(self
, node
):
694 self
.context
.append(len(self
.body
))
695 self
.context
.append('subtitle')
697 def depart_subtitle(self
, node
):
698 style
= self
.context
.pop()
699 start
= self
.context
.pop()
700 self
.append_para(self
.body
[start
:], style
)
701 self
.body
= self
.body
[:start
]
703 def visit_title(self
, node
):
705 self
.context
.append(len(self
.body
))
706 self
.context
.append('title')
707 if isinstance(node
.parent
, nodes
.topic
):
708 self
.context
.append('')
709 self
.topic_class
= 'topic-title'
710 elif self
.sectionlevel
== 0:
711 self
.context
.append('title')
713 self
.context
.append("h%s" % self
.sectionlevel
)
715 if self
.context
[-1] != 'title':
716 if node
.parent
.hasattr('id'):
717 self
.context
.append('</setLink>')
718 self
.body
.append(self
.starttag({}, 'setLink', '', destination
=node
.parent
['id']))
719 if node
.hasattr('refid'):
720 self
.context
.append('</link>')
721 self
.body
.append(self
.starttag({}, 'link', '', destination
=node
['refid']))
723 self
.context
.append('')
725 def depart_title(self
, node
):
726 if node
.hasattr('refid'):
727 self
.body
.append(self
.context
.pop())
728 if node
.parent
.hasattr('id'):
729 self
.body
.append(self
.context
.pop())
730 style
= self
.context
.pop()
732 if isinstance(node
.parent
, nodes
.topic
):
733 style
= self
.topic_class
734 start
= self
.context
.pop()
735 self
.append_para(self
.body
[start
:], style
)
736 self
.body
= self
.body
[:start
]
738 def unimplemented_visit(self
, node
):
739 raise NotImplementedError('visiting unimplemented node type: %s'
740 % node
.__class
__.__name
__)
742 def visit_topic(self
, node
):
743 if node
.hasattr('id'):
744 self
.context
.append('</setLink>')
745 self
.body
.append(self
.starttag({}, 'setLink', '', destination
=node
['id']))
747 def depart_topic(self
, node
):
748 if node
.hasattr('id'):
749 self
.body
.append(self
.context
.pop())
751 def visit_generated(self
, node
):
754 def depart_generated(self
, node
):
757 def invisible_visit(self
, node
):
758 """Invisible nodes should be ignored."""
761 def visit_comment(self
, node
):
764 depart_comment
= invisible_visit
765 visit_substitution_definition
= visit_comment
766 depart_substitution_definition
= depart_comment
767 visit_figure
= visit_comment
768 depart_figure
= depart_comment
770 visit_sidebar
= invisible_visit
771 visit_warning
= invisible_visit
772 visit_tip
= invisible_visit
773 visit_tbody
= invisible_visit
774 visit_thead
= invisible_visit
775 visit_tgroup
= invisible_visit
776 visit_table
= invisible_visit
777 visit_title_reference
= invisible_visit
778 visit_transition
= invisible_visit
779 visit_pending
= invisible_visit
780 depart_pending
= invisible_visit
781 depart_transition
= invisible_visit
782 depart_title_reference
= invisible_visit
783 depart_table
= invisible_visit
784 depart_tgroup
= invisible_visit
785 depart_thead
= invisible_visit
786 depart_tbody
= invisible_visit
787 depart_tip
= invisible_visit
788 depart_warning
= invisible_visit
789 depart_sidebar
= invisible_visit