1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - Export as PDF action 4 5 @copyright: 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin.action import ActionBase 10 from MoinMoin.wikiutil import escape 11 from MoinSupport import ActionSupport, escattr, getFormatterClass, formatText, get_send_headers 12 from os.path import join 13 import subprocess, os, codecs 14 15 __version__ = "0.1" 16 17 Dependencies = ['page'] 18 19 # Configuration settings. 20 21 # Choose one value for the export mode. 22 23 PDF_EXPORT_MODE = "docbook" 24 #PDF_EXPORT_MODE = "wkhtmltopdf" 25 #PDF_EXPORT_MODE = "htmldoc" 26 27 # Settings for "docbook" mode. 28 29 XSLT_PROCESSOR = "/usr/bin/xsltproc" 30 FO_PROCESSOR = "/usr/bin/fop" 31 DOCBOOK_STYLESHEET_BASE = "/usr/share/xml/docbook/stylesheet" 32 33 # Tool settings for "docbook" mode. 34 35 DOCBOOK_TO_FO_STYLESHEET = "docbook-xsl/fo/docbook.xsl" 36 37 # Settings for "wkhtmltopdf" mode. 38 39 XVFB_WRAPPER = "/usr/bin/xvfb-run" 40 WKHTMLTOPDF_PROCESSOR = "/usr/bin/wkhtmltopdf" 41 42 # Settings for "htmldoc" mode. 43 44 HTMLDOC_PROCESSOR = "/usr/bin/htmldoc" 45 46 # NOTE: From docbook-xsl/fo/param.xsl. 47 48 docbook_paper_sizes = [ 49 "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", 50 "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10", 51 "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", 52 "A4landscape", "USletter", "USlandscape", "4A0", "2A0", 53 ] 54 55 docbook_paper_size_labels = { 56 "A4landscape" : "A4 landscape", 57 "USletter" : "US letter", 58 "USlandscape" : "US landscape", 59 "4A0" : "Quadruple A0", 60 "2A0" : "Double A0" 61 } 62 63 wkhtmltopdf_paper_sizes = [ 64 "A4", "Letter" 65 ] 66 67 wkhtmltopdf_paper_size_labels = {} 68 69 # NOTE: From the htmldoc man page. 70 71 htmldoc_paper_sizes = [ 72 "a4", "legal", "letter", "universal" 73 ] 74 75 htmldoc_paper_size_labels = { 76 "a4" : "A4", 77 "legal" : "US legal", 78 "letter" : "US letter", 79 "universal" : "US universal" 80 } 81 82 paper_sizes = { 83 "docbook" : docbook_paper_sizes, 84 "wkhtmltopdf" : wkhtmltopdf_paper_sizes, 85 "htmldoc" : htmldoc_paper_sizes 86 } 87 88 paper_size_labels = { 89 "docbook" : docbook_paper_size_labels, 90 "wkhtmltopdf" : wkhtmltopdf_paper_size_labels, 91 "htmldoc" : htmldoc_paper_size_labels 92 } 93 94 class ExportPDF(ActionBase, ActionSupport): 95 96 "Export the current page as PDF." 97 98 mode = PDF_EXPORT_MODE 99 100 def _get_paper_sizes(self): 101 return paper_sizes.get(self.mode) 102 103 def _get_paper_size_labels(self): 104 return paper_size_labels.get(self.mode) 105 106 def get_form_html(self, buttons_html): 107 108 "Return the action's form incorporating the 'buttons_html'." 109 110 _ = self._ 111 request = self.request 112 form = self.get_form() 113 114 paper_size = form.get("paper-size", ["A4"])[0] 115 116 paper_size_options = [] 117 paper_size_labels = self._get_paper_size_labels() or {} 118 119 for size in self._get_paper_sizes() or []: 120 paper_size_options.append('<option value="%s" %s>%s</option>' % ( 121 escattr(size), self._get_selected(size, paper_size), 122 escape(_(paper_size_labels.get(size) or size)) 123 )) 124 125 d = { 126 "paper_size_label" : escape(_("Paper size")), 127 "paper_size_options" : u"".join(paper_size_options), 128 "buttons_html" : buttons_html, 129 } 130 131 return u"""\ 132 <table> 133 <tr> 134 <td class="label"><label>%(paper_size_label)s</label></td> 135 <td><select name="paper-size">%(paper_size_options)s</select></td> 136 </tr> 137 <tr> 138 <td></td> 139 <td class="buttons">%(buttons_html)s</td> 140 </tr> 141 </table> 142 """ % d 143 144 def do_action(self): 145 146 "Attempt to post a comment." 147 148 _ = self._ 149 form = self.get_form() 150 151 paper_size = form.get("paper-size", [""])[0] 152 153 if not paper_size in self._get_paper_sizes() or []: 154 return 0, _("A paper size must be chosen.") 155 156 if self.mode == "docbook": 157 return self._export_using_docbook(paper_size) 158 elif self.mode == "wkhtmltopdf": 159 return self._export_using_wkhtmltopdf(paper_size) 160 elif self.mode == "htmldoc": 161 return self._export_using_htmldoc(paper_size) 162 else: 163 return 0, _("The action must be configured to use a particular PDF generation tool.") 164 165 def _get_page_as_html(self): 166 167 "Get the page in HTML format." 168 169 request = self.request 170 page = self.page 171 172 fmt = getFormatterClass(request, "text_html")(request) 173 fmt.setPage(page) 174 175 page_as_html = [] 176 append = page_as_html.append 177 178 append("""\ 179 <html> 180 <head> 181 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 182 </head> 183 <body> 184 """) 185 append(formatText(page.get_raw_body(), request, fmt, inhibit_p=False)) 186 append("""\ 187 </body> 188 </html> 189 """) 190 191 return u"".join(page_as_html) 192 193 def _get_page_as_docbook(self): 194 195 "Get the page in DocBook format." 196 197 request = self.request 198 page = self.page 199 200 fmt = getFormatterClass(request, "text_docbook")(request) 201 fmt.setPage(page) 202 203 # The DocBook formatter needs to pretend a full document is being made. 204 205 page_as_docbook = [] 206 append = page_as_docbook.append 207 208 append(fmt.startDocument(page.page_name)) 209 append(fmt.startContent()) 210 append(formatText(page.get_raw_body(), request, fmt, inhibit_p=False).encode("utf-8")) 211 append(fmt.endContent()) 212 append(fmt.endDocument()) 213 214 return "".join(page_as_docbook) 215 216 def _write_pdf_for_html(self, p, page_as_html): 217 218 """ 219 Write to the process 'p', the HTML for the page, reading the PDF output 220 from the process and writing it to the browser. 221 """ 222 223 writer = codecs.getwriter("utf-8")(p.stdin) 224 writer.write(page_as_html) 225 226 out, err = p.communicate() 227 228 retcode = p.wait() 229 230 if retcode != 0: 231 return 0, err 232 233 self._write_pdf(out) 234 return 1, None 235 236 def _export_using_wkhtmltopdf(self, paper_size): 237 238 """ 239 Send the page HTML to the processor, indicating the given 'paper_size'. 240 """ 241 242 p = subprocess.Popen([ 243 XVFB_WRAPPER, "--", 244 WKHTMLTOPDF_PROCESSOR, 245 "--page-size", paper_size, 246 "-", 247 "-" 248 ], 249 shell=False, 250 stdin=subprocess.PIPE, 251 stdout=subprocess.PIPE, 252 stderr=subprocess.PIPE) 253 254 return self._write_pdf_for_html(p, self._get_page_as_html()) 255 256 def _export_using_htmldoc(self, paper_size): 257 258 """ 259 Send the page HTML to the processor, indicating the given 'paper_size'. 260 """ 261 262 os.environ["HTMLDOC_NOCGI"] = "1" 263 264 p = subprocess.Popen([ 265 HTMLDOC_PROCESSOR, 266 "-t", "pdf", "--quiet", "--webpage", 267 "--size", paper_size, 268 "-" 269 ], 270 shell=False, 271 stdin=subprocess.PIPE, 272 stdout=subprocess.PIPE, 273 stderr=subprocess.PIPE) 274 275 return self._write_pdf_for_html(p, self._get_page_as_html()) 276 277 def _export_using_docbook(self, paper_size): 278 279 """ 280 Send the page DocBook XML to the processor, indicating the given 281 'paper_size'. 282 """ 283 284 p1 = subprocess.Popen([ 285 XSLT_PROCESSOR, 286 "-stringparam", "fop1.extensions", "1", 287 "--stringparam", "paper.type", paper_size, 288 join(DOCBOOK_STYLESHEET_BASE, DOCBOOK_TO_FO_STYLESHEET), 289 "-" 290 ], 291 shell=False, 292 stdin=subprocess.PIPE, 293 stdout=subprocess.PIPE, 294 stderr=subprocess.PIPE) 295 296 p1.stdin.write(self._get_page_as_docbook()) 297 p1.stdin.close() 298 299 # Pipe the XML-FO output to the FO processor. 300 301 p2 = subprocess.Popen([ 302 FO_PROCESSOR, 303 "-fo", "-", 304 "-pdf", "-", 305 ], 306 shell=False, 307 stdin=p1.stdout, 308 stdout=subprocess.PIPE, 309 stderr=subprocess.PIPE) 310 311 out, err = p2.communicate() 312 313 retcode = p1.wait() 314 315 if retcode != 0: 316 return 0, err 317 318 retcode = p2.wait() 319 320 if retcode != 0: 321 return 0, err 322 323 self._write_pdf(out) 324 return 1, None 325 326 def _write_pdf(self, out): 327 request = self.request 328 329 send_headers = get_send_headers(request) 330 headers = ["Content-Type: application/pdf"] 331 send_headers(headers) 332 request.write(out) 333 334 def render_success(self, msg, msgtype=None): 335 336 """ 337 Render neither 'msg' nor 'msgtype' since a resource has already been 338 produced. 339 NOTE: msgtype is optional because MoinMoin 1.5.x does not support it. 340 """ 341 342 pass 343 344 # Action invocation function. 345 346 def execute(pagename, request): 347 ExportPDF(pagename, request).render() 348 349 # vim: tabstop=4 expandtab shiftwidth=4