fix get_default_css for changed paths
[objavi2.git] / htdocs / objavi.cgi
blobd7f1244e5d462a74f551521465aab769303af92e
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 os.chdir('..')
26 sys.path.insert(0, os.path.abspath('.'))
27 #print >> sys.stderr, sys.path
29 import re
30 from pprint import pformat
32 from objavi.fmbook import log, Book, make_book_name, HTTP_HOST
33 from objavi import config
34 from objavi.cgi_utils import parse_args, optionise, listify, output_and_exit, font_links, get_default_css
35 from objavi.cgi_utils import output_blob_and_exit, is_utf8, isfloat, isfloat_or_auto, is_isbn
36 from objavi.twiki_wrapper import get_book_list
39 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
40 # functions to validate their values. (None means no validation).
41 ARG_VALIDATORS = {
42 "book": re.compile(r'^([\w-]+/?)*[\w-]+$').match, # can be: BlahBlah/Blah_Blah
43 "css": is_utf8, # an url, empty (for default), or css content
44 "title": lambda x: len(x) < 999 and is_utf8(x),
45 #"header": None, # header text, UNUSED
46 "isbn": is_isbn,
47 "license": config.LICENSES.__contains__,
48 "server": config.SERVER_DEFAULTS.__contains__,
49 "engine": config.ENGINES.__contains__,
50 "booksize": config.PAGE_SIZE_DATA.__contains__,
51 "page_width": isfloat,
52 "page_height": isfloat,
53 "gutter": isfloat_or_auto,
54 "top_margin": isfloat_or_auto,
55 "side_margin": isfloat_or_auto,
56 "bottom_margin": isfloat_or_auto,
57 "columns": isfloat_or_auto,
58 "column_margin": isfloat_or_auto,
59 "cgi-context": lambda x: x.lower() in '1true0false',
60 "mode": config.CGI_MODES.__contains__,
61 "pdftype": lambda x: config.CGI_MODES.get(x, [False])[0],
62 "rotate": u"yes".__eq__,
63 "grey_scale": u"yes".__eq__,
64 "destination": config.CGI_DESTINATIONS.__contains__,
65 "toc_header": is_utf8,
66 "max-age": isfloat,
69 __doc__ += '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS.keys())
72 def get_server_list():
73 return sorted(k for k, v in config.SERVER_DEFAULTS.items() if v['display'])
76 def get_size_list():
77 #order by increasing areal size.
78 def calc_size(name, pointsize, klass):
79 if pointsize:
80 mmx = pointsize[0] * config.POINT_2_MM
81 mmy = pointsize[1] * config.POINT_2_MM
82 return (mmx * mmy, name, klass,
83 '%s (%dmm x %dmm)' % (name, mmx, mmy))
85 return (0, name, klass, name) # presumably 'custom'
87 return [x[1:] for x in sorted(calc_size(k, v.get('pointsize'), v.get('class', ''))
88 for k, v in config.PAGE_SIZE_DATA.iteritems())
91 def make_progress_page(book, bookname, mode, destination='html'):
92 """Return a function that will notify the user of progress. In
93 CGI context this means making an html page to display the
94 messages, which are then sent as javascript snippets on the same
95 connection."""
96 if not CGI_CONTEXT or destination != 'html':
97 return lambda message: '******* got message "%s"' %message
99 print "Content-type: text/html; charset=utf-8\n"
100 f = open(config.PROGRESS_TEMPLATE)
101 template = f.read()
102 f.close()
103 progress_list = ''.join('<li id="%s">%s</li>\n' % x[:2] for x in config.PROGRESS_POINTS
104 if mode in x[2])
106 d = {
107 'book': book,
108 'bookname': bookname,
109 'progress_list': progress_list,
111 print template % d
112 def progress_notifier(message):
113 try:
114 if message.startswith('ERROR:'):
115 log('got an error! %r' % message)
116 print ('<b class="error-message">'
117 '%s\n'
118 '</b></body></html>' % message
120 else:
121 print ('<script type="text/javascript">\n'
122 'objavi_show_progress("%s");\n'
123 '</script>' % message
125 if message == config.FINISHED_MESSAGE:
126 print '</body></html>'
127 sys.stdout.flush()
128 except ValueError, e:
129 log("failed to send message %r, got exception %r" % (message, e))
130 return progress_notifier
133 def get_page_settings(args):
134 """Find the size and any optional layout settings.
136 args['booksize'] is either a keyword describing a size or
137 'custom'. If it is custom, the form is inspected for specific
138 dimensions -- otherwise these are ignored.
140 The margins, gutter, number of columns, and column
141 margins all set themselves automatically based on the page
142 dimensions, but they can be overridden. Any that are are
143 collected here."""
144 # get all the values including sizes first
145 # the sizes are found as 'page_width' and 'page_height',
146 # but the Book class expects them as a 'pointsize' tuple, so
147 # they are easily ignored.
148 settings = {}
149 for k, extrema in config.PAGE_EXTREMA.iteritems():
150 try:
151 v = float(args.get(k))
152 except (ValueError, TypeError):
153 #log("don't like %r as a float value for %s!" % (args.get(k), k))
154 continue
155 min_val, max_val, multiplier = extrema
156 if v < min_val or v > max_val:
157 log('rejecting %s: outside %s' % (v,) + extrema)
158 else:
159 log('found %s=%s' % (k, v))
160 settings[k] = v * multiplier #convert to points in many cases
162 # now if args['size'] is not 'custom', the width and height found
163 # above are ignored.
164 size = args.get('booksize', config.DEFAULT_SIZE)
165 settings.update(config.PAGE_SIZE_DATA[size])
167 #if args['mode'] is 'newspaper', then the number of columns is
168 #automatically determined unless set -- otherwise default is 1.
169 if args.get('mode') == 'newspaper' and settings.get('columns') is None:
170 settings['columns'] = 'auto'
172 if args.get('grey_scale'):
173 settings['grey_scale'] = True
175 if size == 'custom':
176 #will raise KeyError if width, height aren't set
177 settings['pointsize'] = (settings['page_width'], settings['page_height'])
178 del settings['page_width']
179 del settings['page_height']
181 settings['engine'] = args.get('engine', config.DEFAULT_ENGINE)
182 return settings
184 @output_and_exit
185 def mode_booklist(args):
186 return optionise(get_book_list(args.get('server', config.DEFAULT_SERVER)),
187 default=args.get('book'))
189 @output_and_exit
190 def mode_css(args):
191 #XX sending as text/html, but it doesn't really matter
192 return get_default_css(args.get('server', config.DEFAULT_SERVER), args.get('pdftype', 'book'))
195 @output_and_exit
196 def mode_form(args):
197 f = open(config.FORM_TEMPLATE)
198 template = f.read()
199 f.close()
200 f = open(config.FONT_LIST_INCLUDE)
201 font_list = [x.strip() for x in f if x.strip()]
202 f.close()
203 server = args.get('server', config.DEFAULT_SERVER)
204 book = args.get('book')
205 size = args.get('booksize', config.DEFAULT_SIZE)
206 engine = args.get('engine', config.DEFAULT_ENGINE)
207 d = {
208 'server_options': optionise(get_server_list(), default=server),
209 'book_options': optionise(get_book_list(server), default=book),
210 'size_options': optionise(get_size_list(), default=size),
211 'engines': optionise(config.ENGINES.keys(), default=engine),
212 'pdf_types': optionise(sorted(k for k, v in config.CGI_MODES.iteritems() if v[0])),
213 'css': get_default_css(server),
214 'font_links': listify(font_links()),
215 'font_list': listify(font_list),
216 'default_license' : config.DEFAULT_LICENSE,
217 'licenses' : optionise(config.LICENSES, default=config.DEFAULT_LICENSE),
218 'yes': 'yes',
219 None: '',
222 form = []
223 for id, title, type, source, classes, epilogue in config.FORM_INPUTS:
224 val = d.get(source, '')
225 e = config.FORM_ELEMENT_TYPES[type] % locals()
226 form.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
227 '<div class="input_title">%(title)s</div>\n'
228 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
229 '</div>\n' % locals())
231 if True:
232 _valid_inputs = set(ARG_VALIDATORS)
233 _form_inputs = set(x[0] for x in config.FORM_INPUTS if x[2] != 'ul')
234 log("valid but not used inputs: %s" % (_valid_inputs - _form_inputs))
235 log("invalid form inputs: %s" % (_form_inputs - _valid_inputs))
237 return template % {'form': ''.join(form)}
240 def output_multi(book, mimetype, destination):
241 if destination == 'download':
242 f = open(book.publish_file)
243 data = f.read()
244 f.close()
245 output_blob_and_exit(data, mimetype, book.bookname)
246 else:
247 if HTTP_HOST:
248 bookurl = "http://%s/books/%s" % (HTTP_HOST, book.bookname,)
249 else:
250 bookurl = "books/%s" % (book.bookname,)
252 if destination == 'archive.org':
253 details_url, s3url = book.publish_s3()
254 output_blob_and_exit("%s\n%s" % (bookurl, details_url), 'text/plain')
255 elif destination == 'nowhere':
256 output_blob_and_exit(bookurl, 'text/plain')
259 def mode_book(args):
260 # so we're making a pdf.
261 mode = args.get('mode', 'book')
262 bookid = args.get('book')
263 server = args.get('server', config.DEFAULT_SERVER)
264 page_settings = get_page_settings(args)
265 bookname = make_book_name(bookid, server)
266 destination = args.get('destination', config.DEFAULT_CGI_DESTINATION)
267 progress_bar = make_progress_page(bookid, bookname, mode, destination)
269 with Book(bookid, server, bookname, page_settings=page_settings,
270 watchers=[progress_bar], isbn=args.get('isbn'),
271 license=args.get('license'), title=args.get('title'),
272 max_age=float(args.get('max-age', -1))) as book:
273 if CGI_CONTEXT:
274 book.spawn_x()
276 if 'toc_header' in args:
277 book.toc_header = args['toc_header'].decode('utf-8')
278 book.load_book()
279 book.add_css(args.get('css'), mode)
280 book.add_section_titles()
282 if mode == 'book':
283 book.make_book_pdf()
284 elif mode in ('web', 'newspaper'):
285 book.make_simple_pdf(mode)
286 if "rotate" in args:
287 book.rotate180()
289 book.publish_pdf()
290 output_multi(book, "application/pdf", destination)
292 #These ones are similar enough to be handled by the one function
293 mode_newspaper = mode_book
294 mode_web = mode_book
297 def mode_openoffice(args):
298 """Make an openoffice document. A whole lot of the inputs have no
299 effect."""
300 bookid = args.get('book')
301 server = args.get('server', config.DEFAULT_SERVER)
302 bookname = make_book_name(bookid, server, '.odt')
303 destination = args.get('destination', config.DEFAULT_CGI_DESTINATION)
304 progress_bar = make_progress_page(bookid, bookname, 'openoffice', destination)
306 with Book(bookid, server, bookname,
307 watchers=[progress_bar], isbn=args.get('isbn'),
308 license=args.get('license'), title=args.get('title'),
309 max_age=float(args.get('max-age', -1))) as book:
310 if CGI_CONTEXT:
311 book.spawn_x()
312 book.load_book()
313 book.add_css(args.get('css'), 'openoffice')
314 book.add_section_titles()
315 book.make_oo_doc()
316 output_multi(book, "application/vnd.oasis.opendocument.text", destination)
319 def mode_epub(args):
320 log('making epub with\n%s' % pformat(args))
321 #XXX need to catch and process lack of necessary arguments.
322 bookid = args.get('book')
323 server = args.get('server', config.DEFAULT_SERVER)
324 bookname = make_book_name(bookid, server, '.epub')
325 destination = args.get('destination', config.DEFAULT_CGI_DESTINATION)
326 progress_bar = make_progress_page(bookid, bookname, 'epub', destination)
328 with Book(bookid, server, bookname=bookname,
329 watchers=[progress_bar], title=args.get('title'),
330 max_age=float(args.get('max-age', -1))) as book:
332 book.make_epub(use_cache=config.USE_CACHED_IMAGES)
333 output_multi(book, "application/epub+zip", destination)
336 def mode_bookizip(args):
337 log('making bookizip with\n%s' % pformat(args))
338 bookid = args.get('book')
339 server = args.get('server', config.DEFAULT_SERVER)
340 bookname = make_book_name(bookid, server, '.zip')
342 destination = args.get('destination', config.DEFAULT_CGI_DESTINATION)
343 progress_bar = make_progress_page(bookid, bookname, 'bookizip', destination)
345 with Book(bookid, server, bookname=bookname,
346 watchers=[progress_bar], title=args.get('title'),
347 max_age=float(args.get('max-age', -1))) as book:
348 book.publish_bookizip()
349 output_multi(book, config.BOOKIZIP_MIMETYPE, destination)
352 def main():
353 args = parse_args(ARG_VALIDATORS)
354 mode = args.get('mode')
355 if mode is None and 'book' in args:
356 mode = 'book'
358 global CGI_CONTEXT
359 CGI_CONTEXT = 'SERVER_NAME' in os.environ or args.get('cgi-context', 'no').lower() in '1true'
361 if not args and not CGI_CONTEXT:
362 print __doc__
363 sys.exit()
365 output_function = globals().get('mode_%s' % mode, mode_form)
366 output_function(args)
368 CGI_CONTEXT = True
369 if __name__ == '__main__':
370 if config.CGITB_DOMAINS and os.environ.get('REMOTE_ADDR') in config.CGITB_DOMAINS:
371 import cgitb
372 cgitb.enable()
373 main()