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
26 from pprint
import pformat
28 from objavi
.fmbook
import log
, Book
, make_book_name
, HTTP_HOST
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
, is_utf8
, isfloat
, isfloat_or_auto
, is_isbn
32 from objavi
.twiki_wrapper
import get_book_list
34 FORM_TEMPLATE
= os
.path
.abspath('templates/form.html')
35 PROGRESS_TEMPLATE
= os
.path
.abspath('templates/progress.html')
37 # ARG_VALIDATORS is a mapping between the expected cgi arguments and
38 # functions to validate their values. (None means no validation).
40 "book": re
.compile(r
'^([\w-]+/?)*[\w-]+$').match
, # can be: BlahBlah/Blah_Blah
41 "css": is_utf8
, # an url, empty (for default), or css content
42 "title": lambda x
: len(x
) < 999 and is_utf8(x
),
43 #"header": None, # header text, UNUSED
45 "license": config
.LICENSES
.__contains
__,
46 "server": config
.SERVER_DEFAULTS
.__contains
__,
47 "engine": config
.ENGINES
.__contains
__,
48 "booksize": config
.PAGE_SIZE_DATA
.__contains
__,
49 "page_width": isfloat
,
50 "page_height": isfloat
,
51 "gutter": isfloat_or_auto
,
52 "top_margin": isfloat_or_auto
,
53 "side_margin": isfloat_or_auto
,
54 "bottom_margin": isfloat_or_auto
,
55 "columns": isfloat_or_auto
,
56 "column_margin": isfloat_or_auto
,
57 "cgi-context": lambda x
: x
.lower() in '1true0false',
58 "mode": config
.CGI_MODES
.__contains
__,
59 "pdftype": lambda x
: config
.CGI_MODES
.get(x
, [False])[0],
60 "rotate": u
"yes".__eq
__,
61 "grey_scale": u
"yes".__eq
__,
62 "destination": config
.CGI_DESTINATIONS
.__contains
__,
63 "toc_header": is_utf8
,
67 __doc__
+= '\nValid arguments are: %s.\n' % ', '.join(ARG_VALIDATORS
.keys())
70 def get_server_list():
71 return sorted(k
for k
, v
in config
.SERVER_DEFAULTS
.items() if v
['display'])
75 #order by increasing areal size.
76 def calc_size(name
, pointsize
, klass
):
78 mmx
= pointsize
[0] * config
.POINT_2_MM
79 mmy
= pointsize
[1] * config
.POINT_2_MM
80 return (mmx
* mmy
, name
, klass
,
81 '%s (%dmm x %dmm)' % (name
, mmx
, mmy
))
83 return (0, name
, klass
, name
) # presumably 'custom'
85 return [x
[1:] for x
in sorted(calc_size(k
, v
.get('pointsize'), v
.get('class', ''))
86 for k
, v
in config
.PAGE_SIZE_DATA
.iteritems())
89 def get_default_css(server
=config
.DEFAULT_SERVER
, mode
='book'):
90 """Get the default CSS text for the selected server"""
92 cssfile
= config
.SERVER_DEFAULTS
[server
]['css-%s' % mode
]
100 """Links to various example pdfs."""
102 for script
in os
.listdir(config
.FONT_EXAMPLE_SCRIPT_DIR
):
103 if not script
.isalnum():
104 log("warning: font-sample %s won't work; skipping" % script
)
106 links
.append('<a href="%s?script=%s">%s</a>' % (config
.FONT_LIST_URL
, script
, script
))
110 def make_progress_page(book
, bookname
, mode
, destination
='html'):
111 """Return a function that will notify the user of progress. In
112 CGI context this means making an html page to display the
113 messages, which are then sent as javascript snippets on the same
115 if not CGI_CONTEXT
or destination
!= 'html':
116 return lambda message
: '******* got message "%s"' %message
118 print "Content-type: text/html; charset=utf-8\n"
119 f
= open(PROGRESS_TEMPLATE
)
122 progress_list
= ''.join('<li id="%s">%s</li>\n' % x
[:2] for x
in config
.PROGRESS_POINTS
127 'bookname': bookname
,
128 'progress_list': progress_list
,
131 def progress_notifier(message
):
133 if message
.startswith('ERROR:'):
134 log('got an error! %r' % message
)
135 print ('<b class="error-message">'
137 '</b></body></html>' % message
140 print ('<script type="text/javascript">\n'
141 'objavi_show_progress("%s");\n'
142 '</script>' % message
144 if message
== config
.FINISHED_MESSAGE
:
145 print '</body></html>'
147 except ValueError, e
:
148 log("failed to send message %r, got exception %r" % (message
, e
))
149 return progress_notifier
152 def get_page_settings(args
):
153 """Find the size and any optional layout settings.
155 args['booksize'] is either a keyword describing a size or
156 'custom'. If it is custom, the form is inspected for specific
157 dimensions -- otherwise these are ignored.
159 The margins, gutter, number of columns, and column
160 margins all set themselves automatically based on the page
161 dimensions, but they can be overridden. Any that are are
163 # get all the values including sizes first
164 # the sizes are found as 'page_width' and 'page_height',
165 # but the Book class expects them as a 'pointsize' tuple, so
166 # they are easily ignored.
168 for k
, extrema
in config
.PAGE_EXTREMA
.iteritems():
170 v
= float(args
.get(k
))
171 except (ValueError, TypeError):
172 #log("don't like %r as a float value for %s!" % (args.get(k), k))
174 min_val
, max_val
, multiplier
= extrema
175 if v
< min_val
or v
> max_val
:
176 log('rejecting %s: outside %s' % (v
,) + extrema
)
178 log('found %s=%s' % (k
, v
))
179 settings
[k
] = v
* multiplier
#convert to points in many cases
181 # now if args['size'] is not 'custom', the width and height found
183 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
184 settings
.update(config
.PAGE_SIZE_DATA
[size
])
186 #if args['mode'] is 'newspaper', then the number of columns is
187 #automatically determined unless set -- otherwise default is 1.
188 if args
.get('mode') == 'newspaper' and settings
.get('columns') is None:
189 settings
['columns'] = 'auto'
191 if args
.get('grey_scale'):
192 settings
['grey_scale'] = True
195 #will raise KeyError if width, height aren't set
196 settings
['pointsize'] = (settings
['page_width'], settings
['page_height'])
197 del settings
['page_width']
198 del settings
['page_height']
200 settings
['engine'] = args
.get('engine', config
.DEFAULT_ENGINE
)
204 def output_and_exit(f
):
205 """Decorator: prefix function output with http headers and exit
206 immediately after."""
209 print "Content-type: text/html; charset=utf-8\n"
216 def mode_booklist(args
):
217 print optionise(get_book_list(args
.get('server', config
.DEFAULT_SERVER
)),
218 default
=args
.get('book'))
222 #XX sending as text/html, but it doesn't really matter
223 print get_default_css(args
.get('server', config
.DEFAULT_SERVER
), args
.get('pdftype', 'book'))
228 f
= open(FORM_TEMPLATE
)
231 f
= open(config
.FONT_LIST_INCLUDE
)
232 font_list
= [x
.strip() for x
in f
if x
.strip()]
234 server
= args
.get('server', config
.DEFAULT_SERVER
)
235 book
= args
.get('book')
236 size
= args
.get('booksize', config
.DEFAULT_SIZE
)
237 engine
= args
.get('engine', config
.DEFAULT_ENGINE
)
239 'server_options': optionise(get_server_list(), default
=server
),
240 'book_options': optionise(get_book_list(server
), default
=book
),
241 'size_options': optionise(get_size_list(), default
=size
),
242 'engines': optionise(config
.ENGINES
.keys(), default
=engine
),
243 'pdf_types': optionise(sorted(k
for k
, v
in config
.CGI_MODES
.iteritems() if v
[0])),
244 'css': get_default_css(server
),
245 'font_links': listify(font_links()),
246 'font_list': listify(font_list
),
247 'default_license' : config
.DEFAULT_LICENSE
,
248 'licenses' : optionise(config
.LICENSES
, default
=config
.DEFAULT_LICENSE
),
254 for id, title
, type, source
, classes
, epilogue
in config
.FORM_INPUTS
:
255 val
= d
.get(source
, '')
256 e
= config
.FORM_ELEMENT_TYPES
[type] % locals()
257 form
.append('\n<div id="%(id)s_div" class="form-item %(classes)s">\n'
258 '<div class="input_title">%(title)s</div>\n'
259 '<div class="input_contents"> %(e)s %(epilogue)s\n</div>'
260 '</div>\n' % locals())
263 _valid_inputs
= set(ARG_VALIDATORS
)
264 _form_inputs
= set(x
[0] for x
in config
.FORM_INPUTS
if x
[2] != 'ul')
265 log("valid but not used inputs: %s" % (_valid_inputs
- _form_inputs
))
266 log("invalid form inputs: %s" % (_form_inputs
- _valid_inputs
))
268 print template
% {'form': ''.join(form
)}
271 def output_multi(book
, mimetype
, destination
):
272 if destination
== 'download':
273 f
= open(book
.publish_file
)
276 output_blob_and_exit(data
, mimetype
, book
.bookname
)
279 bookurl
= "http://%s/books/%s" % (HTTP_HOST
, book
.bookname
,)
281 bookurl
= "books/%s" % (book
.bookname
,)
283 if destination
== 'archive.org':
284 details_url
, s3url
= book
.publish_s3()
285 output_blob_and_exit("%s\n%s" % (bookurl
, details_url
), 'text/plain')
286 elif destination
== 'nowhere':
287 output_blob_and_exit(bookurl
, 'text/plain')
291 # so we're making a pdf.
292 mode
= args
.get('mode', 'book')
293 bookid
= args
.get('book')
294 server
= args
.get('server', config
.DEFAULT_SERVER
)
295 page_settings
= get_page_settings(args
)
296 bookname
= make_book_name(bookid
, server
)
297 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
298 progress_bar
= make_progress_page(bookid
, bookname
, mode
, destination
)
300 with
Book(bookid
, server
, bookname
, page_settings
=page_settings
,
301 watcher
=progress_bar
, isbn
=args
.get('isbn'),
302 license
=args
.get('license'), title
=args
.get('title'),
303 max_age
=float(args
.get('max-age', -1))) as book
:
307 if 'toc_header' in args
:
308 book
.toc_header
= args
['toc_header'].decode('utf-8')
310 book
.add_css(args
.get('css'), mode
)
311 book
.add_section_titles()
315 elif mode
in ('web', 'newspaper'):
316 book
.make_simple_pdf(mode
)
321 output_multi(book
, "application/pdf", destination
)
323 #These ones are similar enough to be handled by the one function
324 mode_newspaper
= mode_book
328 def mode_openoffice(args
):
329 """Make an openoffice document. A whole lot of the inputs have no
331 bookid
= args
.get('book')
332 server
= args
.get('server', config
.DEFAULT_SERVER
)
333 bookname
= make_book_name(bookid
, server
, '.odt')
334 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
335 progress_bar
= make_progress_page(bookid
, bookname
, 'openoffice', destination
)
337 with
Book(bookid
, server
, bookname
,
338 watcher
=progress_bar
, isbn
=args
.get('isbn'),
339 license
=args
.get('license'), title
=args
.get('title'),
340 max_age
=float(args
.get('max-age', -1))) as book
:
344 book
.add_css(args
.get('css'), 'openoffice')
345 book
.add_section_titles()
347 output_multi(book
, "application/vnd.oasis.opendocument.text", destination
)
351 log('making epub with\n%s' % pformat(args
))
352 #XXX need to catch and process lack of necessary arguments.
353 bookid
= args
.get('book')
354 server
= args
.get('server', config
.DEFAULT_SERVER
)
355 bookname
= make_book_name(bookid
, server
, '.epub')
356 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
357 progress_bar
= make_progress_page(bookid
, bookname
, 'epub', destination
)
359 with
Book(bookid
, server
, bookname
=bookname
,
360 watcher
=progress_bar
, title
=args
.get('title'),
361 max_age
=float(args
.get('max-age', -1))) as book
:
363 book
.make_epub(use_cache
=config
.USE_CACHED_IMAGES
)
364 output_multi(book
, "application/epub+zip", destination
)
367 def mode_bookizip(args
):
368 log('making bookizip with\n%s' % pformat(args
))
369 bookid
= args
.get('book')
370 server
= args
.get('server', config
.DEFAULT_SERVER
)
371 bookname
= make_book_name(bookid
, server
, '.zip')
373 destination
= args
.get('destination', config
.DEFAULT_CGI_DESTINATION
)
374 progress_bar
= make_progress_page(bookid
, bookname
, 'bookizip', destination
)
376 with
Book(bookid
, server
, bookname
=bookname
,
377 watcher
=progress_bar
, title
=args
.get('title'),
378 max_age
=float(args
.get('max-age', -1))) as book
:
379 book
.publish_bookizip()
380 output_multi(book
, config
.BOOKIZIP_MIMETYPE
, destination
)
384 args
= parse_args(ARG_VALIDATORS
)
385 mode
= args
.get('mode')
386 if mode
is None and 'book' in args
:
390 CGI_CONTEXT
= 'SERVER_NAME' in os
.environ
or args
.get('cgi-context', 'no').lower() in '1true'
392 if not args
and not CGI_CONTEXT
:
396 output_function
= globals().get('mode_%s' % mode
, mode_form
)
397 output_function(args
)
399 if __name__
== '__main__':
400 if config
.CGITB_DOMAINS
and os
.environ
.get('REMOTE_ADDR') in config
.CGITB_DOMAINS
: