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