routine to make pages containing ISBN barcodes
[objavi2.git] / objavi2.py
blob59bf929f76b0593a83357d724d34d59c7b307fab
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 cgi
26 import re, time
27 from urllib2 import urlopen
28 from getopt import gnu_getopt
30 from fmbook import log, Book
32 import config
33 from config import SERVER_DEFAULTS, DEFAULT_SERVER
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 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
51 # functions to validate their values. (None means no validation).
52 ARG_VALIDATORS = {
53 "book": re.compile(r'^(\w+/?)*\w+$').match, # can be: BlahBlah/Blah_Blah
54 "css": None, # an url, empty (for default), or css content
55 "title": lambda x: len(x) < 999,
56 "header": None, # header text, UNUSED
57 "isbn": lambda x: x.isdigit() and len(x) == 13,
58 "license": lambda x: len(x) < 999, #should be a codename?
59 "server": SERVER_DEFAULTS.__contains__,
60 "engine": config.ENGINES.__contains__,
61 "booksize": config.PAGE_SIZE_DATA.__contains__,
62 "page_width": isfloat,
63 "page_height": isfloat,
64 "gutter": isfloat_or_auto,
65 "top_margin": isfloat_or_auto,
66 "side_margin": isfloat_or_auto,
67 "bottom_margin": isfloat_or_auto,
68 "columns": isfloat_or_auto,
69 "column_margin": isfloat_or_auto,
70 "cgi-context": lambda x: x.lower() in '1true0false',
71 "mode": str.isalnum,
72 "rotate": u"rotate".__eq__,
75 __doc__ += '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS.keys())
77 def parse_args():
78 """Read and validate CGI or commandline arguments, putting the
79 good ones into the returned dictionary. Command line arguments
80 should be in the form --title='A Book'.
81 """
82 query = cgi.FieldStorage()
83 options, args = gnu_getopt(sys.argv[1:], '', [x + '=' for x in ARG_VALIDATORS])
84 options = dict(options)
85 log(options)
86 data = {}
87 for key, validator in ARG_VALIDATORS.items():
88 value = query.getfirst(key, options.get('--' + key, None))
89 log('%s: %s' % (key, value), debug='STARTUP')
90 if value is not None:
91 if validator is not None and not validator(value):
92 log("argument '%s' is not valid ('%s')" % (key, value))
93 continue
94 data[key] = value
96 log(data, debug='STARTUP')
97 return data
99 def get_server_list():
100 return sorted(SERVER_DEFAULTS.keys())
103 def get_book_list(server):
104 """Ask the server for a list of books. Floss Manual TWikis keep such a list at
105 /bin/view/TWiki/WebLeftBarWebsList?skin=text but it needs a bit of processing
107 If BOOK_LIST_CACHE is non-zero, the book list won't be re-fetched
108 in that many seconds, rather it will be read from disk.
110 if config.BOOK_LIST_CACHE:
111 cache_name = os.path.join(config.BOOK_LIST_CACHE_DIR, '%s.booklist' % server)
112 if (os.path.exists(cache_name) and
113 os.stat(cache_name).st_mtime + config.BOOK_LIST_CACHE > time.time()):
114 f = open(cache_name)
115 s = f.read()
116 f.close()
117 return s.split()
119 url = 'http://%s/bin/view/TWiki/WebLeftBarWebsList?skin=text' % server
120 #XXX should use lxml
121 log(url)
122 f = urlopen(url)
123 s = f.read()
124 f.close()
125 items = sorted(re.findall(r'/bin/view/([\w/]+)/WebHome', s))
126 if config.BOOK_LIST_CACHE:
127 f = open(cache_name, 'w')
128 f.write('\n'.join(items))
129 f.close()
130 return items
132 def get_size_list():
133 #order by increasing areal size.
134 def calc_size(name, pointsize, klass):
135 if pointsize:
136 mmx = pointsize[0] * config.POINT_2_MM
137 mmy = pointsize[1] * config.POINT_2_MM
138 return (mmx * mmy, name, klass,
139 '%s (%dmm x %dmm)' % (name, mmx, mmy))
141 return (0, name, klass, name) # presumably 'custom'
143 return [x[1:] for x in sorted(calc_size(k, v.get('pointsize'), v.get('class', ''))
144 for k, v in config.PAGE_SIZE_DATA.iteritems())
148 def optionise(items, default=None):
149 """Make a list of strings into an html option string, as would fit
150 inside <select> tags."""
151 options = []
152 for x in items:
153 if isinstance(x, str):
154 x = (x, x)
155 if len(x) == 2:
156 # couple: value, name
157 if x[0] == default:
158 options.append('<option selected="selected" value="%s">%s</option>' % x)
159 else:
160 options.append('<option value="%s">%s</option>' % x)
161 else:
162 # triple: value, class, name
163 if x[0] == default:
164 options.append('<option selected="selected" class="%s" value="%s">%s</option>' % x)
165 else:
166 options.append('<option value="%s" class="%s">%s</option>' % x)
168 return '\n'.join(options)
170 def listify(items):
171 """Make a list of strings into html <li> items, to fit in a <ul>
172 or <ol> element."""
173 return '\n'.join('<li>%s</li>' % x for x in items)
176 def get_default_css(server=DEFAULT_SERVER):
177 """Get the default CSS text for the selected server"""
178 log(server)
179 cssfile = SERVER_DEFAULTS[server]['css']
180 log(cssfile)
181 f = open(cssfile)
182 s = f.read()
183 f.close()
184 #log(s)
185 return s
187 def font_links():
188 """Links to various example pdfs."""
189 links = []
190 for script in os.listdir(config.FONT_EXAMPLE_SCRIPT_DIR):
191 if not script.isalnum():
192 log("warning: font-sample %s won't work; skipping" % script)
193 continue
194 links.append('<a href="%s?script=%s">%s</a>' % (config.FONT_LIST_URL, script, script))
195 return links
198 def make_progress_page(book, bookname):
199 f = open(PROGRESS_TEMPLATE)
200 template = f.read()
201 f.close()
202 progress_list = ''.join('<li id="%s">%s</li>\n' for x in config.PROGRESS_POINTS)
204 d = {
205 'book': book,
206 'bookname': bookname,
207 'progress_list': progress_list,
209 print template % d
210 def progress_notifier(message):
211 print ('<script type="text/javascript">\n'
212 'objavi_show_progress("%s");\n'
213 '</script>' % message
215 if message == 'finished':
216 print '</body></html>'
217 sys.stdout.flush()
218 return progress_notifier
220 def print_progress(message):
221 print '******* got message "%s"' %message
223 def make_book_name(book, server):
224 lang = SERVER_DEFAULTS.get(server, SERVER_DEFAULTS[DEFAULT_SERVER])['lang']
225 book = ''.join(x for x in book if x.isalnum())
226 return '%s-%s-%s.pdf' % (book, lang,
227 time.strftime('%Y.%m.%d-%H.%M.%S'))
230 def get_page_settings(args):
231 """Find the size and any optional layout settings.
233 args['booksize'] is either a keyword describing a size or
234 'custom'. If it is custom, the form is inspected for specific
235 dimensions -- otherwise these are ignored.
237 The margins, gutter, number of columns, and column
238 margins all set themselves automatically based on the page
239 dimensions, but they can be overridden. Any that are are
240 collected here."""
241 #get all the values including sizes first
242 settings = {}
243 for k, extrema in config.PAGE_EXTREMA.iteritems():
244 try:
245 v = float(args.get(k))
246 except (ValueError, TypeError):
247 log("don't like %r as a float value for %s!" % (args.get(k), k))
248 continue
249 if v < extrema[0] or v > extrema[1]:
250 log('rejecting %s: outside %s' % (v,) + extrema)
251 else:
252 log('found %s=%s' % (k, v))
253 settings[k] = v * extrema[2]
255 #now if args['size'] is not 'custom', the width height above
256 # is overruled.
258 size = args.get('booksize', config.DEFAULT_SIZE)
259 if size != 'custom':
260 settings.update(config.PAGE_SIZE_DATA[size])
261 else:
262 #will raise KeyError if width, height aren't set
263 settings['pointsize'] = (settings['page_width'], settings['page_height'])
264 del settings['page_width']
265 del settings['page_height']
267 return settings
271 def cgi_context(args):
272 return 'SERVER_NAME' in os.environ or args.get('cgi-context', 'NO').lower() in '1true'
274 def output_and_exit(f):
275 def output(args):
276 if cgi_context(args):
277 print "Content-type: text/html; charset=utf-8\n"
278 f(args)
279 sys.exit()
280 return output
282 @output_and_exit
283 def mode_booklist(args):
284 print optionise(get_book_list(args.get('server', config.DEFAULT_SERVER)),
285 default=args.get('book'))
287 @output_and_exit
288 def mode_css(args):
289 #XX sending as text/html, but it doesn't really matter
290 print get_default_css(args.get('server', config.DEFAULT_SERVER))
293 @output_and_exit
294 def mode_form(args):
295 f = open(FORM_TEMPLATE)
296 template = f.read()
297 f.close()
298 f = open(config.FONT_LIST_INCLUDE)
299 font_list = [x.strip() for x in f if x.strip()]
300 f.close()
301 server = args.get('server', config.DEFAULT_SERVER)
302 book = args.get('book')
303 size = args.get('booksize', config.DEFAULT_SIZE)
304 engine = args.get('engine', config.DEFAULT_ENGINE)
305 d = {
306 'server_options': optionise(get_server_list(), default=server),
307 'book_options': optionise(get_book_list(server), default=book),
308 'size_options': optionise(get_size_list(), default=size),
309 'engines': optionise(config.ENGINES.keys(), default=engine),
310 'css': get_default_css(server),
311 'font_links': listify(font_links()),
312 'font_list': listify(font_list),
313 'default_license' : config.DEFAULT_LICENSE,
314 'licenses' : optionise(config.LICENSES, default=config.DEFAULT_LICENSE),
315 None: '',
318 form = []
319 for id, title, type, source, classes, epilogue in config.FORM_INPUTS:
320 val = d.get(source, '')
321 e = config.FORM_ELEMENT_TYPES[type] % locals()
322 form.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
323 '<div class="input_title">%(title)s</div>\n'
324 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
325 '</div>\n' % locals())
328 print template % {'form': ''.join(form)}
331 @output_and_exit
332 def mode_book(args):
333 # so we're making a book.
334 book = args.get('book')
335 server = args.get('server', config.DEFAULT_SERVER)
336 engine = args.get('engine', config.DEFAULT_ENGINE)
337 page_settings = get_page_settings(args)
338 bookname = make_book_name(book, server)
340 if cgi_context(args):
341 progress_bar = make_progress_page(book, bookname)
342 else:
343 progress_bar = print_progress
345 with Book(book, server, bookname, page_settings=page_settings, engine=engine,
346 watcher=progress_bar) as book:
347 if cgi_context(args):
348 book.spawn_x()
349 book.load()
350 book.set_title(args.get('title'))
351 book.add_css(args.get('css'))
353 book.compose_inside_cover(args.get('license'), args.get('isbn'))
354 book.add_section_titles()
355 book.make_pdf()
357 if "rotate" in args:
358 book.rotate180()
360 book.publish_pdf()
361 book.notify_watcher('finished')
362 #book.cleanup()
366 if __name__ == '__main__':
367 _valid_inputs = set(ARG_VALIDATORS)
368 _form_inputs = set(x[0] for x in config.FORM_INPUTS)
369 log("valid but not used inputs: %s" % (_valid_inputs - _form_inputs))
370 log("invalid form inputs: %s" % (_form_inputs - _valid_inputs))
372 args = parse_args()
373 mode = args.get('mode')
374 if mode is None and 'book' in args:
375 mode = 'book'
377 if not args and not cgi_context(args):
378 print __doc__
379 sys.exit()
381 output_function = globals().get('mode_%s' % mode, mode_form)
382 output_function(args)