use namespace constants, not inline strings
[objavi2.git] / objavi.cgi
blob47114432cdf4a1ec80bc8d28117dba374d65fbd6
1 #!/usr/bin/python
3 # Part of Objavi2, which turns html manuals into books
5 # Copyright (C) 2009 Douglas Bagnall
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 """Make a pdf from the specified book."""
22 from __future__ import with_statement
24 import os, sys
25 import re, time
26 from pprint import pformat
28 from objavi.fmbook import log, Book, ZipBook, make_book_name
29 from objavi import config
30 from objavi.cgi_utils import parse_args, optionise, listify, shift_file
31 from objavi.cgi_utils import output_blob_and_exit
33 from objavi.twiki_wrapper import get_book_list
35 FORM_TEMPLATE = os.path.abspath('templates/form.html')
36 PROGRESS_TEMPLATE = os.path.abspath('templates/progress.html')
38 def isfloat(s):
39 #spaces?, digits!, dot?, digits?, spaces?
40 #return re.compile(r'^\s*[+-]?\d+\.?\d*\s*$').match
41 try:
42 float(s)
43 return True
44 except ValueError:
45 return False
47 def isfloat_or_auto(s):
48 return isfloat(s) or s.lower() in ('', 'auto')
50 def is_isbn(s):
51 # 10 or 13 digits with any number of hyphens, perhaps with check-digit missing
52 s =s.replace('-', '')
53 return (re.match(r'^\d+[\dXx*]$', s) and len(s) in (9, 10, 12, 13))
56 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
57 # functions to validate their values. (None means no validation).
58 ARG_VALIDATORS = {
59 "book": re.compile(r'^(\w+/?)*\w+$').match, # can be: BlahBlah/Blah_Blah
60 "css": None, # an url, empty (for default), or css content
61 "title": lambda x: len(x) < 999,
62 #"header": None, # header text, UNUSED
63 "isbn": is_isbn,
64 "license": config.LICENSES.__contains__,
65 "server": config.SERVER_DEFAULTS.__contains__,
66 "engine": config.ENGINES.__contains__,
67 "booksize": config.PAGE_SIZE_DATA.__contains__,
68 "page_width": isfloat,
69 "page_height": isfloat,
70 "gutter": isfloat_or_auto,
71 "top_margin": isfloat_or_auto,
72 "side_margin": isfloat_or_auto,
73 "bottom_margin": isfloat_or_auto,
74 "columns": isfloat_or_auto,
75 "column_margin": isfloat_or_auto,
76 "cgi-context": lambda x: x.lower() in '1true0false',
77 "mode": config.CGI_MODES.__contains__,
78 "pdftype": lambda x: config.CGI_MODES.get(x, [False])[0],
79 "rotate": u"yes".__eq__,
80 "grey_scale": u"yes".__eq__,
81 "destination": config.EPUB_DESTINATIONS.__contains__,
84 __doc__ += '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS.keys())
87 def get_server_list():
88 return sorted(k for k, v in config.SERVER_DEFAULTS.items() if v['display'])
91 def get_size_list():
92 #order by increasing areal size.
93 def calc_size(name, pointsize, klass):
94 if pointsize:
95 mmx = pointsize[0] * config.POINT_2_MM
96 mmy = pointsize[1] * config.POINT_2_MM
97 return (mmx * mmy, name, klass,
98 '%s (%dmm x %dmm)' % (name, mmx, mmy))
100 return (0, name, klass, name) # presumably 'custom'
102 return [x[1:] for x in sorted(calc_size(k, v.get('pointsize'), v.get('class', ''))
103 for k, v in config.PAGE_SIZE_DATA.iteritems())
106 def get_default_css(server=config.DEFAULT_SERVER, mode='book'):
107 """Get the default CSS text for the selected server"""
108 log(server)
109 cssfile = config.SERVER_DEFAULTS[server]['css-%s' % mode]
110 log(cssfile)
111 f = open(cssfile)
112 s = f.read()
113 f.close()
114 return s
116 def font_links():
117 """Links to various example pdfs."""
118 links = []
119 for script in os.listdir(config.FONT_EXAMPLE_SCRIPT_DIR):
120 if not script.isalnum():
121 log("warning: font-sample %s won't work; skipping" % script)
122 continue
123 links.append('<a href="%s?script=%s">%s</a>' % (config.FONT_LIST_URL, script, script))
124 return links
127 def make_progress_page(book, bookname, mode):
128 """Return a function that will notify the user of progress. In
129 CGI context this means making an html page to display the
130 messages, which are then sent as javascript snippets on the same
131 connection."""
132 if not CGI_CONTEXT:
133 return lambda message: '******* got message "%s"' %message
135 f = open(PROGRESS_TEMPLATE)
136 template = f.read()
137 f.close()
138 progress_list = ''.join('<li id="%s">%s</li>\n' % x[:2] for x in config.PROGRESS_POINTS
139 if mode in x[2])
141 d = {
142 'book': book,
143 'bookname': bookname,
144 'progress_list': progress_list,
146 print template % d
147 def progress_notifier(message):
148 try:
149 print ('<script type="text/javascript">\n'
150 'objavi_show_progress("%s");\n'
151 '</script>' % message
153 if message == 'finished':
154 print '</body></html>'
155 sys.stdout.flush()
156 except ValueError, e:
157 log("failed to send message %r, got exception %r" % (message, e))
158 return progress_notifier
161 def get_page_settings(args):
162 """Find the size and any optional layout settings.
164 args['booksize'] is either a keyword describing a size or
165 'custom'. If it is custom, the form is inspected for specific
166 dimensions -- otherwise these are ignored.
168 The margins, gutter, number of columns, and column
169 margins all set themselves automatically based on the page
170 dimensions, but they can be overridden. Any that are are
171 collected here."""
172 # get all the values including sizes first
173 # the sizes are found as 'page_width' and 'page_height',
174 # but the Book class expects them as a 'pointsize' tuple, so
175 # they are easily ignored.
176 settings = {}
177 for k, extrema in config.PAGE_EXTREMA.iteritems():
178 try:
179 v = float(args.get(k))
180 except (ValueError, TypeError):
181 #log("don't like %r as a float value for %s!" % (args.get(k), k))
182 continue
183 min_val, max_val, multiplier = extrema
184 if v < min_val or v > max_val:
185 log('rejecting %s: outside %s' % (v,) + extrema)
186 else:
187 log('found %s=%s' % (k, v))
188 settings[k] = v * multiplier #convert to points in many cases
190 # now if args['size'] is not 'custom', the width and height found
191 # above are ignored.
192 size = args.get('booksize', config.DEFAULT_SIZE)
193 settings.update(config.PAGE_SIZE_DATA[size])
195 #if args['mode'] is 'newspaper', then the number of columns is
196 #automatically determined unless set -- otherwise default is 1.
197 if args.get('mode') == 'newspaper' and settings.get('columns') is None:
198 settings['columns'] = 'auto'
200 if args.get('grey_scale'):
201 settings['grey_scale'] = True
203 if size == 'custom':
204 #will raise KeyError if width, height aren't set
205 settings['pointsize'] = (settings['page_width'], settings['page_height'])
206 del settings['page_width']
207 del settings['page_height']
209 settings['engine'] = args.get('engine', config.DEFAULT_ENGINE)
210 return settings
213 def output_and_exit(f):
214 """Decorator: prefix function output with http headers and exit
215 immediately after."""
216 def output(args):
217 if CGI_CONTEXT:
218 print "Content-type: text/html; charset=utf-8\n"
219 f(args)
220 sys.exit()
221 return output
223 @output_and_exit
224 def mode_booklist(args):
225 print optionise(get_book_list(args.get('server', config.DEFAULT_SERVER)),
226 default=args.get('book'))
228 @output_and_exit
229 def mode_css(args):
230 #XX sending as text/html, but it doesn't really matter
231 print get_default_css(args.get('server', config.DEFAULT_SERVER), args.get('pdftype', 'book'))
234 @output_and_exit
235 def mode_form(args):
236 f = open(FORM_TEMPLATE)
237 template = f.read()
238 f.close()
239 f = open(config.FONT_LIST_INCLUDE)
240 font_list = [x.strip() for x in f if x.strip()]
241 f.close()
242 server = args.get('server', config.DEFAULT_SERVER)
243 book = args.get('book')
244 size = args.get('booksize', config.DEFAULT_SIZE)
245 engine = args.get('engine', config.DEFAULT_ENGINE)
246 d = {
247 'server_options': optionise(get_server_list(), default=server),
248 'book_options': optionise(get_book_list(server), default=book),
249 'size_options': optionise(get_size_list(), default=size),
250 'engines': optionise(config.ENGINES.keys(), default=engine),
251 'pdf_types': optionise(sorted(k for k, v in config.CGI_MODES.iteritems() if v[0])),
252 'css': get_default_css(server),
253 'font_links': listify(font_links()),
254 'font_list': listify(font_list),
255 'default_license' : config.DEFAULT_LICENSE,
256 'licenses' : optionise(config.LICENSES, default=config.DEFAULT_LICENSE),
257 'yes': 'yes',
258 None: '',
261 form = []
262 for id, title, type, source, classes, epilogue in config.FORM_INPUTS:
263 val = d.get(source, '')
264 e = config.FORM_ELEMENT_TYPES[type] % locals()
265 form.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
266 '<div class="input_title">%(title)s</div>\n'
267 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
268 '</div>\n' % locals())
270 if True:
271 _valid_inputs = set(ARG_VALIDATORS)
272 _form_inputs = set(x[0] for x in config.FORM_INPUTS if x[2] != 'ul')
273 log("valid but not used inputs: %s" % (_valid_inputs - _form_inputs))
274 log("invalid form inputs: %s" % (_form_inputs - _valid_inputs))
276 print template % {'form': ''.join(form)}
279 @output_and_exit
280 def mode_book(args):
281 # so we're making a pdf.
282 mode = args.get('mode', 'book')
283 bookid = args.get('book')
284 server = args.get('server', config.DEFAULT_SERVER)
285 page_settings = get_page_settings(args)
286 bookname = make_book_name(bookid, server)
287 progress_bar = make_progress_page(bookid, bookname, mode)
289 with Book(bookid, server, bookname, page_settings=page_settings,
290 watcher=progress_bar, isbn=args.get('isbn'),
291 license=args.get('license')) as book:
292 if CGI_CONTEXT:
293 book.spawn_x()
294 book.load()
295 book.set_title(args.get('title'))
296 book.add_css(args.get('css'), mode)
297 book.add_section_titles()
299 if mode == 'book':
300 book.make_book_pdf()
301 elif mode in ('web', 'newspaper'):
302 book.make_simple_pdf(mode)
303 if "rotate" in args:
304 book.rotate180()
306 book.publish_pdf()
307 book.notify_watcher('finished')
309 #These ones are similar enough to be handled by the one function
310 mode_newspaper = mode_book
311 mode_web = mode_book
314 @output_and_exit
315 def mode_openoffice(args):
316 """Make an openoffice document. A whole lot of the inputs have no
317 effect."""
318 bookid = args.get('book')
319 server = args.get('server', config.DEFAULT_SERVER)
320 #page_settings = get_page_settings(args)
321 bookname = make_book_name(bookid, server, '.odt')
322 progress_bar = make_progress_page(bookid, bookname, 'openoffice')
324 with Book(bookid, server, bookname,
325 watcher=progress_bar, isbn=args.get('isbn'),
326 license=args.get('license')) as book:
327 if CGI_CONTEXT:
328 book.spawn_x()
329 book.load()
330 book.set_title(args.get('title'))
331 book.add_css(args.get('css'), 'openoffice')
332 book.add_section_titles()
333 book.make_oo_doc()
334 book.notify_watcher('finished')
336 #Not using output_and_exit, because the content type might not be text/html
337 def mode_epub(args):
338 log('making epub with\n%s' % pformat(args))
339 bookid = args.get('book')
340 #server = args.get('server', config.BOOKI_SERVER)
341 server = args.get('server', config.DEFAULT_SERVER)
342 destination = args.get('destination', config.DEFAULT_EPUB_DESTINATION)
343 bookname = '%s.epub' % (bookid,)
345 if destination == 'html':
346 print 'content-type: text/html\n'
347 progress_bar = make_progress_page(bookid, bookname, 'epub')
348 else:
349 progress_bar = None
351 book = ZipBook(server, bookid, watcher=progress_bar)
352 book.make_epub(use_cache=config.USE_CACHED_IMAGES)
353 fn = shift_file(book.epubfile, config.EPUB_DIR)
355 if destination == 'html':
356 book.notify_watcher('finished')
357 elif destination == 'download':
358 f = open(fn)
359 data = f.read()
360 f.close()
361 output_blob_and_exit(data, 'application/epub+zip', bookname)
362 elif destination == 'archive.org':
363 pass
367 def main():
368 args = parse_args(ARG_VALIDATORS)
369 mode = args.get('mode')
370 if mode is None and 'book' in args:
371 mode = 'book'
373 global CGI_CONTEXT
374 CGI_CONTEXT = 'SERVER_NAME' in os.environ or args.get('cgi-context', 'no').lower() in '1true'
376 if not args and not CGI_CONTEXT:
377 print __doc__
378 sys.exit()
380 output_function = globals().get('mode_%s' % mode, mode_form)
381 output_function(args)
383 if __name__ == '__main__':
384 if config.CGITB_DOMAINS and os.environ.get('REMOTE_ADDR') in config.CGITB_DOMAINS:
385 import cgitb
386 cgitb.enable()
387 main()