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